diff --git a/flow-typed/livestream.js b/flow-typed/livestream.js index 40ed13f19..63f1dd6a7 100644 --- a/flow-typed/livestream.js +++ b/flow-typed/livestream.js @@ -24,4 +24,16 @@ declare type LivestreamReplayData = Array; declare type LivestreamState = { fetchingById: {}, viewersById: {}, + fetchingActiveLivestreams: boolean, + activeLivestreams: ?LivestreamInfo, +} + +declare type LivestreamInfo = { + [/* creatorId */ string]: { + live: boolean, + viewCount: number, + creatorId: string, + latestClaimId: string, + latestClaimUri: string, + } } diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 4b02a2128..6baee5377 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -352,3 +352,6 @@ 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'; export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED'; +export const FETCH_ACTIVE_LIVESTREAMS_STARTED = 'FETCH_ACTIVE_LIVESTREAMS_STARTED'; +export const FETCH_ACTIVE_LIVESTREAMS_FAILED = 'FETCH_ACTIVE_LIVESTREAMS_FAILED'; +export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED'; diff --git a/ui/effects/use-get-livestreams.js b/ui/effects/use-get-livestreams.js deleted file mode 100644 index fb77c1395..000000000 --- a/ui/effects/use-get-livestreams.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow -import React from 'react'; -import { LIVESTREAM_LIVE_API } from 'constants/livestream'; - -/** - * Gets latest livestream info list. Returns null (instead of a blank object) - * when there are no active livestreams. - * - * @param minViewers - * @param refreshMs - * @returns {{livestreamMap: null, loading: boolean}} - */ -export default function useGetLivestreams(minViewers: number = 0, refreshMs: number = 0) { - const [loading, setLoading] = React.useState(true); - const [livestreamMap, setLivestreamMap] = React.useState(null); - - React.useEffect(() => { - function checkCurrentLivestreams() { - fetch(LIVESTREAM_LIVE_API) - .then((res) => res.json()) - .then((res) => { - setLoading(false); - if (!res.data) { - setLivestreamMap(null); - return; - } - - const livestreamMap = res.data.reduce((acc, curr) => { - if (curr.viewCount >= minViewers) { - acc[curr.claimId] = curr; - } - return acc; - }, {}); - - setLivestreamMap(livestreamMap); - }) - .catch((err) => { - setLoading(false); - }); - } - - checkCurrentLivestreams(); - - if (refreshMs > 0) { - let fetchInterval = setInterval(checkCurrentLivestreams, refreshMs); - return () => { - if (fetchInterval) { - clearInterval(fetchInterval); - } - }; - } - }, []); - - return { livestreamMap, loading }; -} diff --git a/ui/redux/actions/livestream.js b/ui/redux/actions/livestream.js index c33d3ebc7..9ab7051a4 100644 --- a/ui/redux/actions/livestream.js +++ b/ui/redux/actions/livestream.js @@ -1,6 +1,7 @@ // @flow import * as ACTIONS from 'constants/action_types'; import { doClaimSearch } from 'lbry-redux'; +import { LIVESTREAM_LIVE_API } from 'constants/livestream'; export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ @@ -31,3 +32,75 @@ export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dis }); } }; + +export const doFetchActiveLivestreams = () => { + return async (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED, + }); + + fetch(LIVESTREAM_LIVE_API) + .then((res) => res.json()) + .then((res) => { + if (!res.data) { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, + }); + return; + } + + const activeLivestreams: LivestreamInfo = res.data.reduce((acc, curr) => { + acc[curr.claimId] = { + live: curr.live, + viewCount: curr.viewCount, + creatorId: curr.claimId, + }; + return acc; + }, {}); + + dispatch( + // ** Creators can have multiple livestream claims (each with unique + // chat), and all of them will play the same stream when creator goes + // live. The UI usually just wants to report the latest claim, so we + // query that store it in `latestClaimUri`. + doClaimSearch({ + page_size: 50, + has_no_source: true, + channel_ids: Object.keys(activeLivestreams), + claim_type: ['stream'], + order_by: ['release_time'], // ** + limit_claims_per_channel: 1, // ** + no_totals: true, + }) + ) + .then((resolveInfo) => { + Object.values(resolveInfo).forEach((x) => { + // $FlowFixMe + const channelId = x.stream.signing_channel.claim_id; + activeLivestreams[channelId] = { + ...activeLivestreams[channelId], + // $FlowFixMe + latestClaimId: x.stream.claim_id, + // $FlowFixMe + latestClaimUri: x.stream.canonical_url, + }; + }); + + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED, + data: activeLivestreams, + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, + }); + }); + }) + .catch((err) => { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, + }); + }); + }; +}; diff --git a/ui/redux/reducers/livestream.js b/ui/redux/reducers/livestream.js index 5a74b0a39..932be2cb9 100644 --- a/ui/redux/reducers/livestream.js +++ b/ui/redux/reducers/livestream.js @@ -5,6 +5,8 @@ import { handleActions } from 'util/redux-utils'; const defaultState: LivestreamState = { fetchingById: {}, viewersById: {}, + fetchingActiveLivestreams: false, + activeLivestreams: null, }; export default handleActions( @@ -36,6 +38,16 @@ export default handleActions( newViewersById[claimId] = connected; return { ...state, viewersById: newViewersById }; }, + [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED]: (state: LivestreamState) => { + return { ...state, fetchingActiveLivestreams: true }; + }, + [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED]: (state: LivestreamState) => { + return { ...state, fetchingActiveLivestreams: false }; + }, + [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED]: (state: LivestreamState, action: any) => { + const activeLivestreams: LivestreamInfo = action.data; + return { ...state, fetchingActiveLivestreams: false, activeLivestreams }; + }, }, defaultState ); diff --git a/ui/redux/selectors/livestream.js b/ui/redux/selectors/livestream.js index 27ea16ddb..a48c4e10a 100644 --- a/ui/redux/selectors/livestream.js +++ b/ui/redux/selectors/livestream.js @@ -40,3 +40,23 @@ export const makeSelectPendingLivestreamsForChannelId = (channelId: string) => claim.signing_channel.claim_id === channelId ); }); + +export const selectActiveLivestreams = createSelector(selectState, (state) => state.activeLivestreams); + +export const makeSelectIsActiveLivestream = (uri: string) => + createSelector(selectState, (state) => { + const activeLivestreamValues = (state.activeLivestreams && Object.values(state.activeLivestreams)) || []; + // $FlowFixMe + return Boolean(activeLivestreamValues.find((v) => v.latestClaimUri === uri)); + }); + +export const makeSelectActiveLivestreamUris = (uri: string) => + createSelector(selectState, (state) => { + const activeLivestreamValues = (state.activeLivestreams && Object.values(state.activeLivestreams)) || []; + const uris = []; + activeLivestreamValues.forEach((v) => { + // $FlowFixMe + if (v.latestClaimUri) uris.push(v.latestClaimUri); + }); + return uris; + }); diff --git a/ui/util/livestream.js b/ui/util/livestream.js new file mode 100644 index 000000000..2cf026194 --- /dev/null +++ b/ui/util/livestream.js @@ -0,0 +1,24 @@ +// @flow + +/** + * Helper to extract livestream claim uris from the output of + * `selectActiveLivestreams`. + * + * @param activeLivestreams Object obtained from `selectActiveLivestreams`. + * @param channelIds List of channel IDs to filter the results with. + * @returns {[]|Array<*>} + */ +export function getLivestreamUris(activeLivestreams: ?LivestreamInfo, channelIds: ?Array) { + let values = (activeLivestreams && Object.values(activeLivestreams)) || []; + + if (channelIds && channelIds.length > 0) { + // $FlowFixMe + values = values.filter((v) => channelIds.includes(v.creatorId) && Boolean(v.latestClaimUri)); + } else { + // $FlowFixMe + values = values.filter((v) => Boolean(v.latestClaimUri)); + } + + // $FlowFixMe + return values.map((v) => v.latestClaimUri); +}