use redux for livestream claim setup
This commit is contained in:
parent
b0193202d1
commit
f3463ebdeb
8 changed files with 196 additions and 140 deletions
4
flow-typed/livestream.js
vendored
4
flow-typed/livestream.js
vendored
|
@ -20,3 +20,7 @@ declare type LivestreamReplayItem = {
|
|||
id: string,
|
||||
}
|
||||
declare type LivestreamReplayData = Array<LivestreamReplayItem>;
|
||||
|
||||
declare type LivestreamState = {
|
||||
idsFetching: {},
|
||||
}
|
||||
|
|
|
@ -324,3 +324,8 @@ export const REACTIONS_DISLIKE_COMPLETED = 'REACTIONS_DISLIKE_COMPLETED';
|
|||
export const REPORT_CONTENT_STARTED = 'REPORT_CONTENT_STARTED';
|
||||
export const REPORT_CONTENT_COMPLETED = 'REPORT_CONTENT_COMPLETED';
|
||||
export const REPORT_CONTENT_FAILED = 'REPORT_CONTENT_FAILED';
|
||||
|
||||
// Livestream
|
||||
export const FETCH_NO_SOURCE_CLAIMS_STARTED = 'FETCH_NO_SOURCE_CLAIMS_STARTED';
|
||||
export const FETCH_NO_SOURCE_CLAIMS_COMPLETED = 'FETCH_NO_SOURCE_CLAIMS_COMPLETED';
|
||||
export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED';
|
||||
|
|
|
@ -1,19 +1,34 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectMyChannelClaims, selectFetchingMyChannels, selectPendingClaims, doClearPublish } from 'lbry-redux';
|
||||
import { selectMyChannelClaims, selectFetchingMyChannels, doClearPublish } from 'lbry-redux';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { doFetchNoSourceClaims } from 'redux/actions/livestream';
|
||||
import {
|
||||
makeSelectPendingLivestreamsForChannelId,
|
||||
makeSelectLivestreamsForChannelId,
|
||||
makeSelectIsFetchingLivestreams,
|
||||
} from 'redux/selectors/livestream';
|
||||
import LivestreamSetupPage from './view';
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
const select = (state) => ({
|
||||
const select = (state) => {
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
const { claim_id: channelId, name: channelName } = activeChannelClaim || {};
|
||||
return {
|
||||
channelName,
|
||||
channelId,
|
||||
channels: selectMyChannelClaims(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
pendingClaims: selectPendingClaims(state),
|
||||
});
|
||||
activeChannelClaim,
|
||||
myLivestreamClaims: makeSelectLivestreamsForChannelId(channelId)(state),
|
||||
pendingClaims: makeSelectPendingLivestreamsForChannelId(channelId)(state),
|
||||
fetchingLivestreams: makeSelectIsFetchingLivestreams(channelId)(state),
|
||||
};
|
||||
};
|
||||
const perform = (dispatch) => ({
|
||||
doNewLivestream: (path) => {
|
||||
dispatch(doClearPublish());
|
||||
dispatch(push(path));
|
||||
},
|
||||
fetchNoSourceClaims: (id) => dispatch(doFetchNoSourceClaims(id)),
|
||||
});
|
||||
export default connect(select, perform)(LivestreamSetupPage);
|
||||
|
|
|
@ -16,7 +16,6 @@ import CopyableText from 'component/copyableText';
|
|||
import Card from 'component/common/card';
|
||||
import ClaimList from 'component/claimList';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import usePrevious from 'effects/use-previous';
|
||||
|
||||
type Props = {
|
||||
channels: Array<ChannelClaim>,
|
||||
|
@ -24,47 +23,43 @@ type Props = {
|
|||
activeChannelClaim: ?ChannelClaim,
|
||||
pendingClaims: Array<Claim>,
|
||||
doNewLivestream: (string) => void,
|
||||
fetchNoSourceClaims: (string) => void,
|
||||
myLivestreamClaims: Array<Claim>,
|
||||
fetchingLivestreams: boolean,
|
||||
channelId: ?string,
|
||||
channelName: ?string,
|
||||
};
|
||||
|
||||
export default function LivestreamSetupPage(props: Props) {
|
||||
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000;
|
||||
const { channels, fetchingChannels, activeChannelClaim, pendingClaims, doNewLivestream } = props;
|
||||
const {
|
||||
channels,
|
||||
fetchingChannels,
|
||||
activeChannelClaim,
|
||||
pendingClaims,
|
||||
doNewLivestream,
|
||||
fetchNoSourceClaims,
|
||||
myLivestreamClaims,
|
||||
fetchingLivestreams,
|
||||
channelId,
|
||||
channelName,
|
||||
} = props;
|
||||
|
||||
const [sigData, setSigData] = React.useState({ signature: undefined, signing_ts: undefined });
|
||||
const [showHelpTest, setShowHelpTest] = usePersistedState('livestream-help-seen', true);
|
||||
const [spin, setSpin] = React.useState(true);
|
||||
const [livestreamClaims, setLivestreamClaims] = React.useState([]);
|
||||
const [showHelp, setShowHelp] = usePersistedState('livestream-help-seen', true);
|
||||
|
||||
const hasChannels = channels && channels.length > 0;
|
||||
const activeChannelClaimStr = JSON.stringify(activeChannelClaim);
|
||||
const hasLivestreamClaims = Boolean(myLivestreamClaims.length || pendingClaims.length);
|
||||
|
||||
function createStreamKey() {
|
||||
if (!activeChannelClaim || !sigData.signature || !sigData.signing_ts) return null;
|
||||
return `${activeChannelClaim.claim_id}?d=${toHex(activeChannelClaim.name)}&s=${sigData.signature}&t=${
|
||||
sigData.signing_ts
|
||||
}`;
|
||||
if (!channelId || !channelName || !sigData.signature || !sigData.signing_ts) return null;
|
||||
return `${channelId}?d=${toHex(channelName)}&s=${sigData.signature}&t=${sigData.signing_ts}`;
|
||||
}
|
||||
|
||||
const streamKey = createStreamKey();
|
||||
const pendingLiveStreamClaims = pendingClaims
|
||||
? pendingClaims.filter(
|
||||
(claim) =>
|
||||
// $FlowFixMe
|
||||
claim.value_type === 'stream' && !(claim.value && claim.value.source)
|
||||
)
|
||||
: [];
|
||||
const [localPending, setLocalPending] = React.useState([]); //
|
||||
const localPendingStr = JSON.stringify(localPending);
|
||||
const pendingLivestreamClaimsStr = JSON.stringify(pendingLiveStreamClaims);
|
||||
const prevPendingLiveStreamClaimStr = usePrevious(pendingLivestreamClaimsStr);
|
||||
const liveStreamClaimsStr = JSON.stringify(livestreamClaims);
|
||||
const prevLiveStreamClaimsStr = JSON.stringify(liveStreamClaimsStr);
|
||||
const pendingLength = pendingLiveStreamClaims.length;
|
||||
const totalLivestreamClaims = pendingLiveStreamClaims.concat(livestreamClaims);
|
||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
const localPendingForChannel = localPending.filter(
|
||||
(claim) => claim.signing_channel && claim.signing_channel.claim_id === activeChannelId
|
||||
);
|
||||
|
||||
const pendingLength = pendingClaims.length;
|
||||
const totalLivestreamClaims = pendingClaims.concat(myLivestreamClaims);
|
||||
const helpText = (
|
||||
<div className="section__subtitle">
|
||||
<p>
|
||||
|
@ -104,14 +99,11 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeChannelClaimStr) {
|
||||
const channelClaim = JSON.parse(activeChannelClaimStr);
|
||||
|
||||
// ensure we have a channel
|
||||
if (channelClaim.claim_id) {
|
||||
if (channelId && channelName) {
|
||||
Lbry.channel_sign({
|
||||
channel_id: channelClaim.claim_id,
|
||||
hexdata: toHex(channelClaim.name),
|
||||
channel_id: channelId,
|
||||
hexdata: toHex(channelName),
|
||||
})
|
||||
.then((data) => {
|
||||
setSigData(data);
|
||||
|
@ -120,96 +112,28 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
setSigData({ signature: null, signing_ts: null });
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [activeChannelClaimStr, setSigData]);
|
||||
}, [channelName, channelId, setSigData]);
|
||||
|
||||
// The following 2 effects handle the time between pending disappearing and claim_search being able to find it.
|
||||
// We'll maintain our own pending list:
|
||||
// add to it when there are new things in pending
|
||||
// remove items only when our claim_search finds it
|
||||
React.useEffect(() => {
|
||||
// add to localPending when pending changes
|
||||
const localPending = JSON.parse(localPendingStr);
|
||||
const pendingLivestreamClaims = JSON.parse(pendingLivestreamClaimsStr);
|
||||
if (
|
||||
pendingLiveStreamClaims !== prevPendingLiveStreamClaimStr ||
|
||||
(pendingLivestreamClaims.length && !localPending.length)
|
||||
) {
|
||||
const prevPendingLivestreamClaims = prevPendingLiveStreamClaimStr
|
||||
? JSON.parse(prevPendingLiveStreamClaimStr)
|
||||
: [];
|
||||
const pendingClaimIds = pendingLivestreamClaims.map((claim) => claim.claim_id);
|
||||
const prevPendingClaimIds = prevPendingLivestreamClaims.map((claim) => claim.claim_id);
|
||||
const newLocalPending = [];
|
||||
if (pendingClaimIds.length > prevPendingClaimIds.length) {
|
||||
pendingLivestreamClaims.forEach((pendingClaim) => {
|
||||
if (!localPending.some((lClaim) => lClaim.claim_id === pendingClaim.claim_id)) {
|
||||
newLocalPending.push(pendingClaim);
|
||||
}
|
||||
});
|
||||
setLocalPending(localPending.concat(newLocalPending));
|
||||
}
|
||||
}
|
||||
}, [pendingLivestreamClaimsStr, prevPendingLiveStreamClaimStr, localPendingStr, setLocalPending]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// remove from localPending when livestreamClaims found
|
||||
const localPending = JSON.parse(localPendingStr);
|
||||
if (liveStreamClaimsStr !== prevLiveStreamClaimsStr && localPending.length) {
|
||||
const livestreamClaims = JSON.parse(liveStreamClaimsStr);
|
||||
setLocalPending(
|
||||
localPending.filter((pending) => !livestreamClaims.some((claim) => claim.claim_id === pending.claim_id))
|
||||
);
|
||||
}
|
||||
}, [liveStreamClaimsStr, prevLiveStreamClaimsStr, localPendingStr, setLocalPending]);
|
||||
|
||||
const checkLivestreams = React.useCallback(
|
||||
function checkLivestreamClaims(channelClaimId, setLivestreamClaims, setSpin) {
|
||||
Lbry.claim_search({
|
||||
channel_ids: [channelClaimId],
|
||||
has_no_source: true,
|
||||
claim_type: ['stream'],
|
||||
include_purchase_receipt: true,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.items && res.items.length > 0) {
|
||||
setLivestreamClaims(res.items.reverse());
|
||||
} else {
|
||||
setLivestreamClaims([]);
|
||||
}
|
||||
setSpin(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLivestreamClaims([]);
|
||||
setSpin(false);
|
||||
});
|
||||
},
|
||||
[activeChannelId]
|
||||
);
|
||||
React.useEffect(() => {
|
||||
let checkClaimsInterval;
|
||||
if (!activeChannelClaimStr) return;
|
||||
const channelClaim = JSON.parse(activeChannelClaimStr);
|
||||
if (!channelId) return;
|
||||
|
||||
if (!checkClaimsInterval) {
|
||||
checkLivestreams(channelClaim.claim_id, setLivestreamClaims, setSpin);
|
||||
checkClaimsInterval = setInterval(
|
||||
() => checkLivestreams(channelClaim.claim_id, setLivestreamClaims, setSpin),
|
||||
LIVESTREAM_CLAIM_POLL_IN_MS
|
||||
);
|
||||
fetchNoSourceClaims(channelId);
|
||||
checkClaimsInterval = setInterval(() => fetchNoSourceClaims(channelId), LIVESTREAM_CLAIM_POLL_IN_MS);
|
||||
}
|
||||
return () => {
|
||||
if (checkClaimsInterval) {
|
||||
clearInterval(checkClaimsInterval);
|
||||
}
|
||||
};
|
||||
}, [activeChannelClaimStr, pendingLength, setSpin, checkLivestreams]);
|
||||
}, [channelId, pendingLength, fetchNoSourceClaims]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{(fetchingChannels || spin) && (
|
||||
{fetchingChannels && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -227,21 +151,21 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
{!fetchingChannels && (
|
||||
<div className="section__actions--between">
|
||||
<ChannelSelector hideAnon />
|
||||
<Button button="link" onClick={() => setShowHelpTest(!showHelpTest)} label={__('How does this work?')} />
|
||||
<Button button="link" onClick={() => setShowHelp(!showHelp)} label={__('How does this work?')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{spin && !fetchingChannels && (
|
||||
{fetchingLivestreams && !fetchingChannels && !hasLivestreamClaims && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div className="card-stack">
|
||||
{!spin && !fetchingChannels && activeChannelClaim && (
|
||||
{hasLivestreamClaims && !fetchingChannels && channelId && (
|
||||
<>
|
||||
{showHelpTest && (
|
||||
{showHelp && (
|
||||
<Card
|
||||
titleActions={<Button button="close" icon={ICONS.REMOVE} onClick={() => setShowHelpTest(false)} />}
|
||||
titleActions={<Button button="close" icon={ICONS.REMOVE} onClick={() => setShowHelp(false)} />}
|
||||
title={__('Go Live on Odysee')}
|
||||
subtitle={__(`You're invited to try out our new livestreaming service while in beta!`)}
|
||||
actions={helpText}
|
||||
|
@ -274,15 +198,15 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
|
||||
{totalLivestreamClaims.length > 0 ? (
|
||||
<>
|
||||
{Boolean(localPendingForChannel.length) && (
|
||||
{Boolean(pendingClaims.length) && (
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={__('Your pending livestream uploads')}
|
||||
uris={localPendingForChannel.map((claim) => claim.permanent_url)}
|
||||
uris={pendingClaims.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(livestreamClaims.length) && (
|
||||
{Boolean(myLivestreamClaims.length) && (
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={__('Your livestream uploads')}
|
||||
|
@ -292,7 +216,7 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
check_again: (
|
||||
<Button
|
||||
button="link"
|
||||
onClick={() => checkLivestreams(activeChannelId, setLivestreamClaims, setSpin)}
|
||||
onClick={() => fetchNoSourceClaims(channelId)}
|
||||
label={__('Check again')}
|
||||
/>
|
||||
),
|
||||
|
@ -301,8 +225,10 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
Nothing here yet. %check_again%
|
||||
</I18nMessage>
|
||||
}
|
||||
uris={livestreamClaims
|
||||
.filter((c) => !pendingLiveStreamClaims.some((p) => p.permanent_url === c.permanent_url))
|
||||
uris={myLivestreamClaims
|
||||
.filter(
|
||||
(claim) => !pendingClaims.some((pending) => pending.permanent_url === claim.permanent_url)
|
||||
)
|
||||
.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -327,8 +253,7 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
<Button
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
setSpin(true);
|
||||
checkLivestreams(activeChannelId, setLivestreamClaims, setSpin);
|
||||
fetchNoSourceClaims(channelId);
|
||||
}}
|
||||
label={__('Check again...')}
|
||||
/>
|
||||
|
@ -338,7 +263,7 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
)}
|
||||
|
||||
{/* Debug Stuff */}
|
||||
{streamKey && false && (
|
||||
{streamKey && false && activeChannelClaim && (
|
||||
<div style={{ marginTop: 'var(--spacing-l)' }}>
|
||||
<h3>Debug Info</h3>
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import userReducer from 'redux/reducers/user';
|
|||
import commentsReducer from 'redux/reducers/comments';
|
||||
import blockedReducer from 'redux/reducers/blocked';
|
||||
import coinSwapReducer from 'redux/reducers/coinSwap';
|
||||
import livestreamReducer from 'redux/reducers/livestream';
|
||||
import searchReducer from 'redux/reducers/search';
|
||||
import reactionsReducer from 'redux/reducers/reactions';
|
||||
import syncReducer from 'redux/reducers/sync';
|
||||
|
@ -30,6 +31,7 @@ export default (history) =>
|
|||
costInfo: costInfoReducer,
|
||||
fileInfo: fileInfoReducer,
|
||||
homepage: homepageReducer,
|
||||
livestream: livestreamReducer,
|
||||
notifications: notificationsReducer,
|
||||
publish: publishReducer,
|
||||
reactions: reactionsReducer,
|
||||
|
|
33
ui/redux/actions/livestream.js
Normal file
33
ui/redux/actions/livestream.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { doClaimSearch } from 'lbry-redux';
|
||||
|
||||
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED,
|
||||
data: channelId,
|
||||
});
|
||||
|
||||
const items = await dispatch(
|
||||
doClaimSearch({
|
||||
channel_ids: [channelId],
|
||||
has_no_source: true,
|
||||
claim_type: ['stream'],
|
||||
no_totals: true,
|
||||
page_size: 20,
|
||||
page: 1,
|
||||
include_is_my_output: true,
|
||||
})
|
||||
);
|
||||
if (items) {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_COMPLETED,
|
||||
data: channelId,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_FAILED,
|
||||
data: channelId,
|
||||
});
|
||||
}
|
||||
};
|
34
ui/redux/reducers/livestream.js
Normal file
34
ui/redux/reducers/livestream.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { handleActions } from 'util/redux-utils';
|
||||
|
||||
const defaultState: LivestreamState = {
|
||||
idsFetching: {},
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED]: (state: LivestreamState, action: any): LivestreamState => {
|
||||
const claimId = action.data;
|
||||
const newIdsFetching = Object.assign({}, state.idsFetching);
|
||||
newIdsFetching[claimId] = true;
|
||||
|
||||
return { ...state, idsFetching: newIdsFetching };
|
||||
},
|
||||
[ACTIONS.FETCH_NO_SOURCE_CLAIMS_COMPLETED]: (state: LivestreamState, action: any): LivestreamState => {
|
||||
const claimId = action.data;
|
||||
const newIdsFetching = Object.assign({}, state.idsFetching);
|
||||
newIdsFetching[claimId] = false;
|
||||
|
||||
return { ...state, idsFetching: newIdsFetching };
|
||||
},
|
||||
[ACTIONS.FETCH_NO_SOURCE_CLAIMS_FAILED]: (state: LivestreamState, action: any) => {
|
||||
const claimId = action.data;
|
||||
const newIdsFetching = Object.assign({}, state.idsFetching);
|
||||
newIdsFetching[claimId] = false;
|
||||
|
||||
return { ...state, idsFetching: newIdsFetching };
|
||||
},
|
||||
},
|
||||
defaultState
|
||||
);
|
38
ui/redux/selectors/livestream.js
Normal file
38
ui/redux/selectors/livestream.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
// @flow
|
||||
import { createSelector } from 'reselect';
|
||||
import { selectMyClaims, selectPendingClaims } from 'lbry-redux';
|
||||
|
||||
const selectState = (state) => state.livestream || {};
|
||||
|
||||
// select non-pending claims without sources for given channel
|
||||
export const makeSelectLivestreamsForChannelId = (channelId: string) =>
|
||||
createSelector(selectState, selectMyClaims, (livestreamState, myClaims = []) => {
|
||||
return myClaims
|
||||
.filter(
|
||||
(claim) =>
|
||||
claim.value_type === 'stream' &&
|
||||
claim.value &&
|
||||
!claim.value.source &&
|
||||
claim.confirmations > 0 &&
|
||||
claim.signing_channel &&
|
||||
claim.signing_channel.claim_id === channelId
|
||||
)
|
||||
.sort((a, b) => b.timestamp - a.timestamp); // newest first
|
||||
});
|
||||
|
||||
export const selectFetchingLivestreams = createSelector(selectState, (state) => state.idsFetching);
|
||||
|
||||
export const makeSelectIsFetchingLivestreams = (channelId: string) =>
|
||||
createSelector(selectFetchingLivestreams, (fetchingLivestreams) => Boolean(fetchingLivestreams[channelId]));
|
||||
|
||||
export const makeSelectPendingLivestreamsForChannelId = (channelId: string) =>
|
||||
createSelector(selectPendingClaims, (pendingClaims) => {
|
||||
return pendingClaims.filter(
|
||||
(claim) =>
|
||||
claim.value_type === 'stream' &&
|
||||
claim.value &&
|
||||
!claim.value.source &&
|
||||
claim.signing_channel &&
|
||||
claim.signing_channel.claim_id === channelId
|
||||
);
|
||||
});
|
Loading…
Reference in a new issue