Livestream category improvements #7115

Merged
infinite-persistence merged 15 commits from ip/category.livestream into master 2021-09-24 16:26:22 +02:00
7 changed files with 144 additions and 55 deletions
Showing only changes of commit b29f56d53f - Show all commits

View file

@ -24,4 +24,16 @@ declare type LivestreamReplayData = Array<LivestreamReplayItem>;
declare type LivestreamState = { declare type LivestreamState = {
fetchingById: {}, fetchingById: {},
viewersById: {}, viewersById: {},
fetchingActiveLivestreams: boolean,
activeLivestreams: ?LivestreamInfo,
}
declare type LivestreamInfo = {
[/* creatorId */ string]: {
live: boolean,
viewCount: number,
creatorId: string,
latestClaimId: string,
latestClaimUri: string,
}
} }

View file

@ -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_COMPLETED = 'FETCH_NO_SOURCE_CLAIMS_COMPLETED';
export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED'; export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED';
export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED'; 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';

View file

@ -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 };
}

View file

@ -1,6 +1,7 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import { doClaimSearch } from 'lbry-redux'; import { doClaimSearch } from 'lbry-redux';
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => { export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ 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,
});
});
};
};

View file

@ -5,6 +5,8 @@ import { handleActions } from 'util/redux-utils';
const defaultState: LivestreamState = { const defaultState: LivestreamState = {
fetchingById: {}, fetchingById: {},
viewersById: {}, viewersById: {},
fetchingActiveLivestreams: false,
activeLivestreams: null,
}; };
export default handleActions( export default handleActions(
@ -36,6 +38,16 @@ export default handleActions(
newViewersById[claimId] = connected; newViewersById[claimId] = connected;
return { ...state, viewersById: newViewersById }; 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 defaultState
); );

View file

@ -40,3 +40,23 @@ export const makeSelectPendingLivestreamsForChannelId = (channelId: string) =>
claim.signing_channel.claim_id === channelId 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;
});

24
ui/util/livestream.js Normal file
View file

@ -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<string>) {
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);
}