Add ability to link to latest or current live channel file pages
- and some changes to activeLivestream redux since it would return undefined if fetching and no claim, so now it returns null when no activeLivestream is found
This commit is contained in:
parent
bf158ad696
commit
6b2427768c
11 changed files with 145 additions and 13 deletions
|
@ -53,6 +53,9 @@ export const IS_MAC = navigator.userAgent.indexOf('Mac OS X') !== -1;
|
|||
// const imaLibraryPath = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
|
||||
const oneTrustScriptSrc = 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js';
|
||||
|
||||
const LATEST_PATH = `/$/${PAGES.LATEST}/`;
|
||||
const LIVE_PATH = `/$/${PAGES.LIVE_NOW}/`;
|
||||
|
||||
type Props = {
|
||||
language: string,
|
||||
languages: Array<string>,
|
||||
|
@ -151,8 +154,22 @@ function App(props: Props) {
|
|||
const connectionStatus = useConnectionStatus();
|
||||
|
||||
const urlPath = pathname + hash;
|
||||
const latestContentPath = urlPath.startsWith(LATEST_PATH);
|
||||
const liveContentPath = urlPath.startsWith(LIVE_PATH);
|
||||
const isNewestPath = latestContentPath || liveContentPath;
|
||||
|
||||
let path = urlPath.slice(1).replace(/:/g, '#');
|
||||
let path;
|
||||
if (isNewestPath) {
|
||||
path = urlPath.replace(latestContentPath ? LATEST_PATH : LIVE_PATH, '');
|
||||
} else {
|
||||
// Remove the leading "/" added by the browser
|
||||
path = urlPath.slice(1);
|
||||
}
|
||||
path = path.replace(/:/g, '#');
|
||||
|
||||
if (isNewestPath && !path.startsWith('@')) {
|
||||
path = `@${path}`;
|
||||
}
|
||||
|
||||
if (search && search.startsWith('?q=cache:')) {
|
||||
generateGoogleCacheUrl(search, path);
|
||||
|
|
|
@ -401,6 +401,8 @@ function AppRouter(props: Props) {
|
|||
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
|
||||
|
||||
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
||||
<Route path={`/$/${PAGES.LATEST}/:channelName`} exact render={() => <ShowPage uri={uri} latestContentPath />} />
|
||||
<Route path={`/$/${PAGES.LIVE_NOW}/:channelName`} exact render={() => <ShowPage uri={uri} liveContentPath />} />
|
||||
<Route path="/:claimName" exact render={() => <ShowPage uri={uri} />} />
|
||||
<Route path="/:claimName/:streamName" exact render={() => <ShowPage uri={uri} />} />
|
||||
<Route path="/*" component={FourOhFourPage} />
|
||||
|
|
|
@ -333,6 +333,8 @@ export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
|
|||
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
|
||||
export const FETCH_LAST_ACTIVE_SUBS_SKIP = 'FETCH_LAST_ACTIVE_SUBS_SKIP';
|
||||
export const FETCH_LAST_ACTIVE_SUBS_DONE = 'FETCH_LAST_ACTIVE_SUBS_DONE';
|
||||
export const FETCH_LATEST_FOR_CHANNEL_DONE = 'FETCH_LATEST_FOR_CHANNEL_DONE';
|
||||
export const FETCH_LATEST_FOR_CHANNEL_FAIL = 'FETCH_LATEST_FOR_CHANNEL_FAIL';
|
||||
export const FETCH_LAST_ACTIVE_SUBS_FAIL = 'FETCH_LAST_ACTIVE_SUBS_FAIL';
|
||||
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
|
||||
|
||||
|
|
|
@ -92,3 +92,5 @@ exports.GENERAL = 'general';
|
|||
exports.LIST = 'list';
|
||||
exports.ODYSEE_MEMBERSHIP = 'membership';
|
||||
exports.POPOUT = 'popout';
|
||||
exports.LATEST = 'latest';
|
||||
exports.LIVE_NOW = 'livenow';
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
selectClaimIsMine,
|
||||
makeSelectClaimIsPending,
|
||||
selectGeoRestrictionForUri,
|
||||
selectLatestClaimByUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import {
|
||||
makeSelectCollectionForId,
|
||||
|
@ -13,7 +14,7 @@ import {
|
|||
makeSelectIsResolvingCollectionForId,
|
||||
} from 'redux/selectors/collections';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { doResolveUri, doFetchLatestClaimForChannel } from 'redux/actions/claims';
|
||||
import { doBeginPublish } from 'redux/actions/publish';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { doFetchItemsInCollection } from 'redux/actions/collections';
|
||||
|
@ -21,10 +22,12 @@ import { isStreamPlaceholderClaim } from 'util/claim';
|
|||
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
|
||||
import { selectBlacklistedOutpointMap } from 'lbryinc';
|
||||
import { selectActiveLiveClaimForChannel } from 'redux/selectors/livestream';
|
||||
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
|
||||
import ShowPage from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { uri, location } = props;
|
||||
const { uri, location, liveContentPath } = props;
|
||||
const { search } = location;
|
||||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
@ -35,9 +38,16 @@ const select = (state, props) => {
|
|||
(claim && claim.value_type === 'collection' && claim.claim_id) ||
|
||||
null;
|
||||
|
||||
const { canonical_url: canonicalUrl, claim_id: claimId } = claim || {};
|
||||
const latestContentClaim = liveContentPath
|
||||
? selectActiveLiveClaimForChannel(state, claimId)
|
||||
: selectLatestClaimByUri(state, canonicalUrl);
|
||||
const latestClaimUrl = latestContentClaim && latestContentClaim.canonical_url;
|
||||
|
||||
return {
|
||||
uri,
|
||||
claim,
|
||||
latestClaimUrl,
|
||||
isResolvingUri: selectIsUriResolving(state, uri),
|
||||
blackListedOutpointMap: selectBlacklistedOutpointMap(state),
|
||||
isSubscribed: selectIsSubscribedForUri(state, uri),
|
||||
|
@ -58,6 +68,8 @@ const perform = {
|
|||
doBeginPublish,
|
||||
doFetchItemsInCollection,
|
||||
doOpenModal,
|
||||
fetchLatestClaimForChannel: doFetchLatestClaimForChannel,
|
||||
fetchChannelLiveStatus: doFetchChannelLiveStatus,
|
||||
};
|
||||
|
||||
export default withRouter(connect(select, perform)(ShowPage));
|
||||
|
|
|
@ -38,7 +38,12 @@ type Props = {
|
|||
isResolvingCollection: boolean,
|
||||
isAuthenticated: boolean,
|
||||
geoRestriction: ?GeoRestriction,
|
||||
doResolveUri: (uri: string, returnCached: boolean, resolveReposts: boolean, options: any) => void,
|
||||
latestContentPath?: boolean,
|
||||
liveContentPath?: boolean,
|
||||
latestClaimUrl: ?string,
|
||||
fetchLatestClaimForChannel: (uri: string) => void,
|
||||
fetchChannelLiveStatus: (channelId: string) => void,
|
||||
doResolveUri: (uri: string, returnCached?: boolean, resolveReposts?: boolean, options?: any) => void,
|
||||
doBeginPublish: (name: ?string) => void,
|
||||
doFetchItemsInCollection: ({ collectionId: string }) => void,
|
||||
doOpenModal: (string, {}) => void,
|
||||
|
@ -61,6 +66,11 @@ export default function ShowPage(props: Props) {
|
|||
isResolvingCollection,
|
||||
isAuthenticated,
|
||||
geoRestriction,
|
||||
latestContentPath,
|
||||
liveContentPath,
|
||||
latestClaimUrl,
|
||||
fetchLatestClaimForChannel,
|
||||
fetchChannelLiveStatus,
|
||||
doResolveUri,
|
||||
doBeginPublish,
|
||||
doFetchItemsInCollection,
|
||||
|
@ -77,6 +87,8 @@ export default function ShowPage(props: Props) {
|
|||
const claimExists = claim !== null && claim !== undefined;
|
||||
const haventFetchedYet = claim === undefined;
|
||||
const isMine = claim && claim.is_my_output;
|
||||
const claimId = claim && claim.claim_id;
|
||||
const isNewestPath = latestContentPath || liveContentPath;
|
||||
|
||||
const { contentName, isChannel } = parseURI(uri); // deprecated contentName - use streamName and channelName
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
|
@ -90,6 +102,26 @@ export default function ShowPage(props: Props) {
|
|||
blackListedOutpointMap[`${claim.txid}:${claim.nout}`]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canonicalUrl && isNewestPath) {
|
||||
doResolveUri(uri);
|
||||
}
|
||||
// only for mount on a latest content page
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestClaimUrl && liveContentPath && claimId) {
|
||||
fetchChannelLiveStatus(claimId);
|
||||
}
|
||||
}, [claimId, fetchChannelLiveStatus, latestClaimUrl, liveContentPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestClaimUrl && latestContentPath && canonicalUrl) {
|
||||
fetchLatestClaimForChannel(canonicalUrl);
|
||||
}
|
||||
}, [canonicalUrl, fetchLatestClaimForChannel, latestClaimUrl, latestContentPath]);
|
||||
|
||||
// changed this from 'isCollection' to resolve strangers' collections.
|
||||
useEffect(() => {
|
||||
if (collectionId && !resolvedCollection) {
|
||||
|
@ -150,9 +182,24 @@ export default function ShowPage(props: Props) {
|
|||
isAuthenticated,
|
||||
]);
|
||||
|
||||
// Wait for latest claim fetch
|
||||
if (isNewestPath && latestClaimUrl === undefined) {
|
||||
return (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNewestPath && latestClaimUrl) {
|
||||
const params = urlParams.toString() !== '' ? `?${urlParams.toString()}` : '';
|
||||
return <Redirect to={`${formatLbryUrlForWeb(latestClaimUrl)}${params}`} />;
|
||||
}
|
||||
|
||||
// Don't navigate directly to repost urls
|
||||
// Always redirect to the actual content
|
||||
if (claim && claim.repost_url === uri) {
|
||||
// Also redirect to channel page (uri) when on a non-existing latest path (live or content)
|
||||
if (claim && (claim.repost_url === uri || (isNewestPath && latestClaimUrl === null))) {
|
||||
const newUrl = formatLbryUrlForWeb(canonicalUrl);
|
||||
return <Redirect to={newUrl} />;
|
||||
}
|
||||
|
|
|
@ -1134,3 +1134,23 @@ export const doCheckPendingClaims = (onChannelConfirmed: Function) => (dispatch:
|
|||
checkTxoList();
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
export const doFetchLatestClaimForChannel = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||
const searchOptions = {
|
||||
limit_claims_per_channel: 1,
|
||||
channel: uri,
|
||||
no_totals: true,
|
||||
order_by: ['release_time'],
|
||||
page: 1,
|
||||
has_source: true,
|
||||
};
|
||||
|
||||
return dispatch(doClaimSearch(searchOptions))
|
||||
.then((results) =>
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_LATEST_FOR_CHANNEL_DONE,
|
||||
data: { uri, results },
|
||||
})
|
||||
)
|
||||
.catch(() => dispatch({ type: ACTIONS.FETCH_LATEST_FOR_CHANNEL_FAIL }));
|
||||
};
|
||||
|
|
|
@ -61,6 +61,7 @@ type State = {
|
|||
isCheckingNameForPublish: boolean,
|
||||
checkingPending: boolean,
|
||||
checkingReflecting: boolean,
|
||||
latestByUri: { [string]: any },
|
||||
};
|
||||
|
||||
const reducers = {};
|
||||
|
@ -109,6 +110,7 @@ const defaultState = {
|
|||
isCheckingNameForPublish: false,
|
||||
checkingPending: false,
|
||||
checkingReflecting: false,
|
||||
latestByUri: {},
|
||||
};
|
||||
|
||||
// ****************************************************************************
|
||||
|
@ -929,6 +931,17 @@ reducers[ACTIONS.PURCHASE_LIST_STARTED] = (state: State): State => {
|
|||
};
|
||||
};
|
||||
|
||||
reducers[ACTIONS.FETCH_LATEST_FOR_CHANNEL_DONE] = (state: State, action: any): State => {
|
||||
const { uri, results } = action.data;
|
||||
const latestByUri = Object.assign({}, state.latestByUri);
|
||||
latestByUri[uri] = results;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
...state,
|
||||
latestByUri,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[ACTIONS.PURCHASE_LIST_COMPLETED] = (state: State, action: any): State => {
|
||||
const { result }: { result: PurchaseListResponse, resolve: boolean } = action.data;
|
||||
const page = result.page;
|
||||
|
|
|
@ -7,7 +7,7 @@ const defaultState: LivestreamState = {
|
|||
fetchingById: {},
|
||||
viewersById: {},
|
||||
fetchingActiveLivestreams: 'pending',
|
||||
activeLivestreams: null,
|
||||
activeLivestreams: {},
|
||||
activeLivestreamsLastFetchedDate: 0,
|
||||
activeLivestreamsLastFetchedOptions: {},
|
||||
activeLivestreamsLastFetchedFailCount: 0,
|
||||
|
@ -95,9 +95,9 @@ export default handleActions(
|
|||
};
|
||||
},
|
||||
[ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS]: (state: LivestreamState, action: any) => {
|
||||
const activeLivestreams = state.activeLivestreams;
|
||||
if (activeLivestreams) delete activeLivestreams[action.data.channelId];
|
||||
return { ...state, activeLivestreams: Object.assign({}, activeLivestreams), activeLivestreamInitialized: true };
|
||||
const activeLivestreams = Object.assign({}, state.activeLivestreams);
|
||||
activeLivestreams[action.data.channelId] = null;
|
||||
return { ...state, activeLivestreams, activeLivestreamInitialized: true };
|
||||
},
|
||||
[ACTIONS.SOCKET_CONNECTED_BY_ID]: (state: LivestreamState, action: any) => {
|
||||
const { connected, sub_category, id: claimId } = action.data;
|
||||
|
|
|
@ -33,6 +33,17 @@ export const selectCreatingChannel = (state: State) => selectState(state).creati
|
|||
export const selectCreateChannelError = (state: State) => selectState(state).createChannelError;
|
||||
export const selectRepostLoading = (state: State) => selectState(state).repostLoading;
|
||||
export const selectRepostError = (state: State) => selectState(state).repostError;
|
||||
export const selectLatestByUri = (state: State) => selectState(state).latestByUri;
|
||||
|
||||
export const selectLatestClaimByUri = createSelector(
|
||||
(state, uri) => uri,
|
||||
selectLatestByUri,
|
||||
(uri, latestByUri) => {
|
||||
const latestClaim = latestByUri[uri];
|
||||
// $FlowFixMe
|
||||
return latestClaim && Object.values(latestClaim)[0].stream;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectClaimsByUri = createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => {
|
||||
const claims = {};
|
||||
|
@ -810,7 +821,7 @@ export const selectIsMyChannelCountOverLimit = createSelector(
|
|||
* @param uri
|
||||
* @returns {*}
|
||||
*/
|
||||
export const selectOdyseeMembershipForUri = function (state: State, uri: string) {
|
||||
export const selectOdyseeMembershipForUri = (state: State, uri: string) => {
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
|
||||
const uploaderChannelClaimId = getChannelIdFromClaim(claim);
|
||||
|
@ -834,7 +845,7 @@ export const selectOdyseeMembershipForUri = function (state: State, uri: string)
|
|||
* @param channelId
|
||||
* @returns {*}
|
||||
*/
|
||||
export const selectOdyseeMembershipForChannelId = function (state: State, channelId: string) {
|
||||
export const selectOdyseeMembershipForChannelId = (state: State, channelId: string) => {
|
||||
// looks for the uploader id
|
||||
const matchingMembershipOfUser =
|
||||
state.user && state.user.odyseeMembershipsPerClaimIds && state.user.odyseeMembershipsPerClaimIds[channelId];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { createSelector } from 'reselect';
|
||||
import { createCachedSelector } from 're-reselect';
|
||||
import { selectMyClaims, selectPendingClaims } from 'redux/selectors/claims';
|
||||
import { selectMyClaims, selectPendingClaims, selectClaimForUri } from 'redux/selectors/claims';
|
||||
|
||||
type State = { livestream: any };
|
||||
|
||||
|
@ -91,6 +91,12 @@ export const selectActiveLivestreamForChannel = createCachedSelector(
|
|||
if (!channelId || !activeLivestreams) {
|
||||
return null;
|
||||
}
|
||||
return activeLivestreams[channelId] || null;
|
||||
return activeLivestreams[channelId];
|
||||
}
|
||||
)((state, channelId) => String(channelId));
|
||||
|
||||
export const selectActiveLiveClaimForChannel = createCachedSelector(
|
||||
(state) => state,
|
||||
selectActiveLivestreamForChannel,
|
||||
(state, activeLivestream) => activeLivestream && selectClaimForUri(state, activeLivestream.claimUri)
|
||||
)((state, channelId) => String(channelId));
|
||||
|
|
Loading…
Reference in a new issue