Add stream start to active live streams, and refactor how data active claim is stored in redux

This commit is contained in:
Dan Peterson 2021-12-22 10:12:44 -06:00 committed by Thomas Zarebczan
parent 72c1a8ae25
commit d382671616
18 changed files with 192 additions and 203 deletions

View file

@ -21,17 +21,6 @@ declare type LivestreamReplayItem = {
}
declare type LivestreamReplayData = Array<LivestreamReplayItem>;
declare type CurrentLiveClaim = {
claimId: string | null,
claimUri: string | null,
}
declare type LivestreamChannelStatus = {
channelId: null | string,
isBroadcasting: boolean,
liveClaim: CurrentLiveClaim,
}
declare type LivestreamState = {
fetchingById: {},
viewersById: {},
@ -39,8 +28,7 @@ declare type LivestreamState = {
activeLivestreams: ?LivestreamInfo,
activeLivestreamsLastFetchedDate: number,
activeLivestreamsLastFetchedOptions: {},
currentChannelStatus: LivestreamChannelStatus,
activeLivestreamInitialized: boolean,
}
declare type LivestreamInfo = {

View file

@ -14,8 +14,8 @@ import { withRouter } from 'react-router';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
import { doFetchActiveLivestream } from 'redux/actions/livestream';
import { selectCurrentChannelStatus } from 'redux/selectors/livestream';
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
import { getChannelIdFromClaim } from 'util/claim';
import ChannelContent from './view';
const select = (state, props) => {
@ -23,6 +23,7 @@ const select = (state, props) => {
const urlParams = new URLSearchParams(search);
const page = urlParams.get('page') || 0;
const claim = props.uri && selectClaimForUri(state, props.uri);
const channelClaimId = getChannelIdFromClaim(claim);
return {
pageOfClaimsInChannel: makeSelectClaimsInChannelForPage(props.uri, page)(state),
@ -34,7 +35,8 @@ const select = (state, props) => {
isAuthenticated: selectUserVerifiedEmail(state),
showMature: selectShowMatureContent(state),
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
currentChannelStatus: selectCurrentChannelStatus(state),
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
};
};

View file

@ -38,7 +38,8 @@ type Props = {
claimType: string,
empty?: string,
doFetchActiveLivestream: (string) => void,
currentChannelStatus: LivestreamChannelStatus,
activeLivestreamForChannel: any,
activeLivestreamInitialized: boolean,
};
function ChannelContent(props: Props) {
@ -59,7 +60,8 @@ function ChannelContent(props: Props) {
claimType,
empty,
doFetchActiveLivestream,
currentChannelStatus,
activeLivestreamForChannel,
activeLivestreamInitialized,
} = props;
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
const claimsInChannel = 9999;
@ -252,8 +254,8 @@ function ChannelContent(props: Props) {
setSearchResults(null);
}, [url]);
const [isInitialized, setIsInitialized] = React.useState(false);
const [isChannelBroadcasting, setIsChannelBroadcasting] = React.useState(false);
const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized;
const isChannelBroadcasting = Boolean(activeLivestreamForChannel);
// Find out current channels status + active live claim.
React.useEffect(() => {
@ -262,14 +264,6 @@ function ChannelContent(props: Props) {
return () => clearInterval(intervalId);
}, [claimId, doFetchActiveLivestream]);
React.useEffect(() => {
const initialized = currentChannelStatus.channelId === claimId;
setIsInitialized(initialized);
if (initialized) {
setIsChannelBroadcasting(currentChannelStatus.isBroadcasting);
}
}, [currentChannelStatus, claimId]);
const showScheduledLiveStreams = claimType !== 'collection'; // ie. not on the playlist page.
return (
@ -279,7 +273,7 @@ function ChannelContent(props: Props) {
)}
{!fetching && isInitialized && isChannelBroadcasting && !isChannelEmpty && (
<LivestreamLink claimUri={currentChannelStatus.liveClaim.claimUri} />
<LivestreamLink claimUri={activeLivestreamForChannel.claimUri} />
)}
{!fetching && showScheduledLiveStreams && (
@ -287,7 +281,7 @@ function ChannelContent(props: Props) {
channelIds={[claimId]}
tileLayout={tileLayout}
liveUris={
isChannelBroadcasting && currentChannelStatus.liveClaim ? [currentChannelStatus.liveClaim.claimUri] : []
isChannelBroadcasting && activeLivestreamForChannel.claimUri ? [activeLivestreamForChannel.claimUri] : []
}
/>
)}

View file

@ -1,5 +0,0 @@
import { createContext } from 'react';
const ClaimListDiscoverContext = createContext();
export default ClaimListDiscoverContext;

View file

@ -5,7 +5,6 @@ import { doClearPublish, doPrepareEdit } from 'redux/actions/publish';
import { push } from 'connected-react-router';
import ClaimPreviewSubtitle from './view';
import { doFetchSubCount, selectSubCountForUri } from 'lbryinc';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
const select = (state, props) => {
const claim = selectClaimForUri(state, props.uri);
@ -16,7 +15,6 @@ const select = (state, props) => {
pending: makeSelectClaimIsPending(props.uri)(state),
isLivestream,
subCount: isChannel ? selectSubCountForUri(state, props.uri) : 0,
isLivestreamActive: isLivestream && selectIsActiveLivestreamForUri(state, props.uri),
};
};

View file

@ -1,13 +1,12 @@
// @flow
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import React, { useContext } from 'react';
import React from 'react';
import UriIndicator from 'component/uriIndicator';
import DateTime from 'component/dateTime';
import LivestreamDateTime from 'component/livestreamDateTime';
import Button from 'component/button';
import FileViewCountInline from 'component/fileViewCountInline';
import { parseURI } from 'util/lbryURI';
import ClaimListDiscoverContext from 'component/claimListDiscover/context';
import moment from 'moment';
type Props = {
uri: string,
@ -18,12 +17,11 @@ type Props = {
isLivestream: boolean,
fetchSubCount: (string) => void,
subCount: number,
isLivestreamActive: boolean,
};
// previews used in channel overview and homepage (and other places?)
function ClaimPreviewSubtitle(props: Props) {
const { pending, uri, claim, type, beginPublish, isLivestream, isLivestreamActive, fetchSubCount, subCount } = props;
const { pending, uri, claim, type, beginPublish, isLivestream, fetchSubCount, subCount } = props;
const isChannel = claim && claim.value_type === 'channel';
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
@ -41,29 +39,6 @@ function ClaimPreviewSubtitle(props: Props) {
({ streamName: name } = parseURI(uri));
} catch (e) {}
const { listingType } = useContext(ClaimListDiscoverContext) || {};
const LivestreamDateTimeLabel = () => {
// If showing in upcoming and in the past. (we allow x time in past to show here if not live yet)
if (listingType === 'UPCOMING') {
// $FlowFixMe
if (moment.unix(claim.value.release_time).isBefore()) {
return __('Starting Soon');
}
} else {
// If not in upcoming + live and in the future (started streaming a bit early)
// $FlowFixMe
if (isLivestreamActive && moment.unix(claim.value.release_time).isAfter()) {
return __('Streaming Now');
}
}
return (
<>
{__('Livestream')} <DateTime timeAgo uri={uri} />
</>
);
};
return (
<div className="media__subtitle">
{claim ? (
@ -82,7 +57,7 @@ function ClaimPreviewSubtitle(props: Props) {
{!isChannel &&
(isLivestream && ENABLE_NO_SOURCE_CLAIMS ? (
<LivestreamDateTimeLabel />
<LivestreamDateTime uri={uri} />
) : (
<>
<FileViewCountInline uri={uri} isLivestream={isLivestream} />

View file

@ -1,11 +1,12 @@
// @flow
import React, { useContext } from 'react';
import React from 'react';
import classnames from 'classnames';
import { NavLink, withRouter } from 'react-router-dom';
import FileThumbnail from 'component/fileThumbnail';
import UriIndicator from 'component/uriIndicator';
import TruncatedText from 'component/common/truncated-text';
import DateTime from 'component/dateTime';
import LivestreamDateTime from 'component/livestreamDateTime';
import ChannelThumbnail from 'component/channelThumbnail';
import FileViewCountInline from 'component/fileViewCountInline';
import SubscribeButton from 'component/subscribeButton';
@ -19,8 +20,6 @@ import FileWatchLaterLink from 'component/fileWatchLaterLink';
import ClaimRepostAuthor from 'component/claimRepostAuthor';
import ClaimMenuList from 'component/claimMenuList';
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
import ClaimListDiscoverContext from 'component/claimListDiscover/context';
import moment from 'moment';
// $FlowFixMe cannot resolve ...
import PlaceholderTx from 'static/img/placeholderTx.gif';
@ -110,8 +109,6 @@ function ClaimPreviewTile(props: Props) {
}
}
const { listingType } = useContext(ClaimListDiscoverContext) || {};
const signingChannel = claim && claim.signing_channel;
const isChannel = claim && claim.value_type === 'channel';
const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
@ -175,23 +172,6 @@ function ClaimPreviewTile(props: Props) {
liveProperty = (claim) => <>LIVE</>;
}
const LivestreamDateTimeLabel = () => {
// If showing in upcoming and in the past. (we allow x time in past to show here if not live yet)
if (listingType === 'UPCOMING') {
// $FlowFixMe
if (moment.unix(claim.value.release_time).isBefore()) {
return __('Starting Soon');
}
} else {
// If not in upcoming + live and in the future (started streaming a bit early)
// $FlowFixMe
if (isLivestreamActive && moment.unix(claim.value.release_time).isAfter()) {
return __('Streaming Now');
}
}
return <DateTime timeAgo uri={uri} />;
};
return (
<li
onClick={handleClick}
@ -260,7 +240,7 @@ function ClaimPreviewTile(props: Props) {
<UriIndicator uri={uri} link />
<div className="claim-tile__about--counts">
<FileViewCountInline uri={uri} isLivestream={isLivestream} />
{isLivestream && <LivestreamDateTimeLabel />}
{isLivestream && <LivestreamDateTime uri={uri} />}
{!isLivestream && <DateTime timeAgo uri={uri} />}
</div>
</div>

View file

@ -4,6 +4,7 @@ import DateTime from 'component/dateTime';
import FileViewCount from 'component/fileViewCount';
import FileActions from 'component/fileActions';
import ClaimPreviewReset from 'component/claimPreviewReset';
import LivestreamDateTime from 'component/livestreamDateTime';
type Props = {
uri: string,
@ -13,13 +14,12 @@ type Props = {
function FileSubtitle(props: Props) {
const { uri, livestream = false, isLive = false } = props;
const showDateTime = !livestream || (livestream && !isLive);
return (
<>
<div className="media__subtitle--between">
<div className="file__viewdate">
{showDateTime && <DateTime uri={uri} show={DateTime.SHOW_DATE} />}
{livestream && isLive && <LivestreamDateTime uri={uri} />}
{!livestream && <DateTime uri={uri} show={DateTime.SHOW_DATE} />}
<FileViewCount uri={uri} livestream={livestream} isLive={isLive} />
</div>

View file

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import LivestreamDateTime from './view';
import { selectActiveLivestreamForUri } from 'redux/selectors/livestream';
const select = (state, props) => {
const claim = props.uri && makeSelectClaimForUri(props.uri)(state);
return {
claim,
activeLivestream: selectActiveLivestreamForUri(state, props.uri),
};
};
export default connect(select)(LivestreamDateTime);

View file

@ -0,0 +1,37 @@
// @flow
import React from 'react';
import DateTime from 'component/dateTime';
import { LIVESTREAM_STARTED_RECENTLY_BUFFER } from 'constants/livestream';
import moment from 'moment';
type Props = {
uri: string,
claim: any,
activeLivestream: any,
};
const LivestreamDateTime = (props: Props) => {
const { uri, claim, activeLivestream } = props;
if (activeLivestream) {
return (
<span>
{__('Started')} <DateTime timeAgo date={activeLivestream.startedStreaming.toDate()} />
</span>
);
}
if (
moment
.unix(claim.value.release_time)
.isBetween(moment().subtract(LIVESTREAM_STARTED_RECENTLY_BUFFER, 'minutes'), moment())
) {
return __('Starting Soon');
}
return (
<span>
{__('Live')} <DateTime timeAgo uri={uri} />
</span>
);
};
export default LivestreamDateTime;

View file

@ -80,7 +80,12 @@ export default function LivestreamLayout(props: Props) {
</div>
)}
{activeStreamUri && <LivestreamLink claimUri={activeStreamUri} />}
{activeStreamUri && (
<LivestreamLink
title={__("Click here to access the stream that's currently active")}
claimUri={activeStreamUri}
/>
)}
<React.Suspense fallback={null}>{isMobile && !hideComments && <LivestreamComments uri={uri} />}</React.Suspense>

View file

@ -7,17 +7,18 @@ import { useHistory } from 'react-router';
import { formatLbryUrlForWeb } from 'util/url';
type Props = {
title?: string,
claimUri: string,
};
export default function LivestreamLink(props: Props) {
const { claimUri } = props;
const { claimUri, title = null } = props;
const { push } = useHistory();
const element = (props: { children: any }) => (
<Card
className="livestream__channel-link claim-preview__live"
title={__('Live stream in progress')}
title={title || __('Live stream in progress')}
onClick={() => {
push(formatLbryUrlForWeb(claimUri));
}}

View file

@ -8,7 +8,6 @@ import { useIsMediumScreen, useIsLargeScreen } from 'effects/use-screensize';
import ClaimListDiscover from 'component/claimListDiscover';
import Button from 'component/button';
import { LIVESTREAM_UPCOMING_BUFFER } from 'constants/livestream';
import ClaimListDiscoverContext from 'component/claimListDiscover/context';
type Props = {
channelIds: Array<string>,
@ -39,7 +38,6 @@ const ScheduledStreams = (props: Props) => {
};
return (
<ClaimListDiscoverContext.Provider value={{ listingType: 'UPCOMING' }}>
<div className={'mb-xl'} style={{ display: showUpcomingLivestreams ? 'block' : 'none' }}>
<ClaimListDiscover
useSkeletonScreen={false}
@ -88,7 +86,6 @@ const ScheduledStreams = (props: Props) => {
</div>
)}
</div>
</ClaimListDiscoverContext.Provider>
);
};

View file

@ -6,16 +6,20 @@ 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 { selectCurrentChannelStatus } from 'redux/selectors/livestream';
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
import { doFetchActiveLivestream } from 'redux/actions/livestream';
import LivestreamPage from './view';
const select = (state, props) => ({
const select = (state, props) => {
const channelClaimId = getChannelIdFromClaim(selectClaimForUri(state, props.uri));
return {
isAuthenticated: selectUserVerifiedEmail(state),
channelClaimId: getChannelIdFromClaim(selectClaimForUri(state, props.uri)),
channelClaimId,
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
currentChannelStatus: selectCurrentChannelStatus(state),
});
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
};
};
const perform = {
doSetPlayingUri,

View file

@ -20,7 +20,8 @@ type Props = {
doCommentSocketConnect: (string, string) => void,
doCommentSocketDisconnect: (string) => void,
doFetchActiveLivestream: (string) => void,
currentChannelStatus: LivestreamChannelStatus,
activeLivestreamForChannel: any,
activeLivestreamInitialized: boolean,
};
export default function LivestreamPage(props: Props) {
@ -35,7 +36,8 @@ export default function LivestreamPage(props: Props) {
doCommentSocketConnect,
doCommentSocketDisconnect,
doFetchActiveLivestream,
currentChannelStatus,
activeLivestreamForChannel,
activeLivestreamInitialized,
} = props;
React.useEffect(() => {
@ -58,10 +60,9 @@ export default function LivestreamPage(props: Props) {
};
}, [claimId, uri, doCommentSocketConnect, doCommentSocketDisconnect]);
const [isInitialized, setIsInitialized] = React.useState(false);
const [isChannelBroadcasting, setIsChannelBroadcasting] = React.useState(false);
const [isCurrentClaimLive, setIsCurrentClaimLive] = React.useState(false);
const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized;
const isChannelBroadcasting = Boolean(activeLivestreamForChannel);
const isCurrentClaimLive = isChannelBroadcasting && activeLivestreamForChannel.claimId === claimId;
const livestreamChannelId = channelClaimId || '';
// Find out current channels status + active live claim.
@ -71,19 +72,10 @@ export default function LivestreamPage(props: Props) {
return () => clearInterval(intervalId);
}, [livestreamChannelId, doFetchActiveLivestream]);
React.useEffect(() => {
const initialized = currentChannelStatus.channelId === livestreamChannelId;
setIsInitialized(initialized);
if (initialized) {
setIsChannelBroadcasting(currentChannelStatus.isBroadcasting);
setIsCurrentClaimLive(currentChannelStatus.liveClaim.claimId === claimId);
}
}, [currentChannelStatus, livestreamChannelId, claimId]);
const [activeStreamUri, setActiveStreamUri] = React.useState(false);
React.useEffect(() => {
setActiveStreamUri(!isCurrentClaimLive && isChannelBroadcasting ? currentChannelStatus.liveClaim.claimUri : false);
setActiveStreamUri(!isCurrentClaimLive && isChannelBroadcasting ? activeLivestreamForChannel.claimUri : false);
}, [isCurrentClaimLive, isChannelBroadcasting]); // eslint-disable-line react-hooks/exhaustive-deps
// $FlowFixMe

View file

@ -165,14 +165,18 @@ export const doFetchActiveLivestream = (channelId: string) => {
const liveChannel = await fetchLiveChannel(channelId);
const currentlyLiveClaims = await findActiveStreams([channelId], ['release_time'], liveChannel, dispatch);
const liveClaim = currentlyLiveClaims[channelId];
liveChannel[channelId].claimId = liveClaim.stream.claim_id;
liveChannel[channelId].claimUri = liveClaim.stream.canonical_url;
dispatch({
type: ACTIONS.FETCH_ACTIVE_LIVESTREAM_COMPLETED,
data: { liveClaim: { claimId: liveClaim.stream.claim_id, claimUri: liveClaim.stream.canonical_url } },
data: {
...liveChannel,
},
});
} catch (err) {
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAM_FAILED });
} finally {
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAM_FINISHED, data: { channelId } });
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAM_FAILED, data: { channelId } });
}
};
};

View file

@ -3,15 +3,6 @@
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const currentChannelStatus: LivestreamChannelStatus = {
channelId: null,
isBroadcasting: false,
liveClaim: {
claimId: null,
claimUri: null,
},
};
const defaultState: LivestreamState = {
fetchingById: {},
viewersById: {},
@ -19,10 +10,7 @@ const defaultState: LivestreamState = {
activeLivestreams: null,
activeLivestreamsLastFetchedDate: 0,
activeLivestreamsLastFetchedOptions: {},
currentChannelStatus: {
...currentChannelStatus,
},
activeLivestreamInitialized: false,
};
export default handleActions(
@ -70,24 +58,14 @@ export default handleActions(
activeLivestreamsLastFetchedOptions,
};
},
[ACTIONS.FETCH_ACTIVE_LIVESTREAM_COMPLETED]: (state: LivestreamState, action: any) => {
const currentChannelStatus = Object.assign({}, state.currentChannelStatus, {
isBroadcasting: true,
liveClaim: action.data.liveClaim,
});
return { ...state, currentChannelStatus };
const activeLivestreams = Object.assign({}, state.activeLivestreams || {}, action.data);
return { ...state, activeLivestreams, activeLivestreamInitialized: true };
},
[ACTIONS.FETCH_ACTIVE_LIVESTREAM_FAILED]: (state: LivestreamState) => {
const currentChannelStatus = Object.assign({}, state.currentChannelStatus, {
isBroadcasting: false,
liveClaim: { claimId: null, claimUri: null },
});
return { ...state, currentChannelStatus };
},
[ACTIONS.FETCH_ACTIVE_LIVESTREAM_FINISHED]: (state: LivestreamState, action: any) => {
const currentChannelStatus = Object.assign({}, state.currentChannelStatus, { channelId: action.data.channelId });
return { ...state, currentChannelStatus };
[ACTIONS.FETCH_ACTIVE_LIVESTREAM_FAILED]: (state: LivestreamState, action: any) => {
const activeLivestreams = state.activeLivestreams;
if (activeLivestreams) delete activeLivestreams[action.data.channelId];
return { ...state, activeLivestreams: Object.assign({}, activeLivestreams), activeLivestreamInitialized: true };
},
},
defaultState

View file

@ -62,6 +62,31 @@ export const selectIsActiveLivestreamForUri = createCachedSelector(
}
)((state, uri) => String(uri));
export const selectActiveLivestreamForUri = createCachedSelector(
(state, uri) => uri,
selectActiveLivestreams,
(uri, activeLivestreams) => {
if (!uri || !activeLivestreams) {
return null;
}
const activeLivestreamValues = Object.values(activeLivestreams);
// $FlowFixMe - unable to resolve claimUri
return activeLivestreamValues.find((v) => v.claimUri === uri) || null;
}
)((state, uri) => String(uri));
export const selectActiveLivestreamForChannel = createCachedSelector(
(state, channelId) => channelId,
selectActiveLivestreams,
(channelId, activeLivestreams) => {
if (!channelId || !activeLivestreams) {
return null;
}
return activeLivestreams[channelId] || null;
}
)((state, channelId) => String(channelId));
export const selectFetchingActiveLivestreams = (state: State) => selectState(state).fetchingActiveLivestreams;
export const selectCurrentChannelStatus = (state: State) => selectState(state).currentChannelStatus;
export const selectActiveLivestreamInitialized = (state: State) => selectState(state).activeLivestreamInitialized;