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:
Rafael 2022-05-09 09:26:04 -03:00 committed by Thomas Zarebczan
parent bf158ad696
commit 6b2427768c
11 changed files with 145 additions and 13 deletions

View file

@ -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 imaLibraryPath = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
const oneTrustScriptSrc = 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js'; const oneTrustScriptSrc = 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js';
const LATEST_PATH = `/$/${PAGES.LATEST}/`;
const LIVE_PATH = `/$/${PAGES.LIVE_NOW}/`;
type Props = { type Props = {
language: string, language: string,
languages: Array<string>, languages: Array<string>,
@ -151,8 +154,22 @@ function App(props: Props) {
const connectionStatus = useConnectionStatus(); const connectionStatus = useConnectionStatus();
const urlPath = pathname + hash; 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:')) { if (search && search.startsWith('?q=cache:')) {
generateGoogleCacheUrl(search, path); generateGoogleCacheUrl(search, path);

View file

@ -401,6 +401,8 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} /> <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 */} {/* 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" exact render={() => <ShowPage uri={uri} />} />
<Route path="/:claimName/:streamName" exact render={() => <ShowPage uri={uri} />} /> <Route path="/:claimName/:streamName" exact render={() => <ShowPage uri={uri} />} />
<Route path="/*" component={FourOhFourPage} /> <Route path="/*" component={FourOhFourPage} />

View file

@ -333,6 +333,8 @@ export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS'; 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_SKIP = 'FETCH_LAST_ACTIVE_SUBS_SKIP';
export const FETCH_LAST_ACTIVE_SUBS_DONE = 'FETCH_LAST_ACTIVE_SUBS_DONE'; 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 FETCH_LAST_ACTIVE_SUBS_FAIL = 'FETCH_LAST_ACTIVE_SUBS_FAIL';
export const SET_VIEW_MODE = 'SET_VIEW_MODE'; export const SET_VIEW_MODE = 'SET_VIEW_MODE';

View file

@ -92,3 +92,5 @@ exports.GENERAL = 'general';
exports.LIST = 'list'; exports.LIST = 'list';
exports.ODYSEE_MEMBERSHIP = 'membership'; exports.ODYSEE_MEMBERSHIP = 'membership';
exports.POPOUT = 'popout'; exports.POPOUT = 'popout';
exports.LATEST = 'latest';
exports.LIVE_NOW = 'livenow';

View file

@ -6,6 +6,7 @@ import {
selectClaimIsMine, selectClaimIsMine,
makeSelectClaimIsPending, makeSelectClaimIsPending,
selectGeoRestrictionForUri, selectGeoRestrictionForUri,
selectLatestClaimByUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { import {
makeSelectCollectionForId, makeSelectCollectionForId,
@ -13,7 +14,7 @@ import {
makeSelectIsResolvingCollectionForId, makeSelectIsResolvingCollectionForId,
} from 'redux/selectors/collections'; } from 'redux/selectors/collections';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; 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 { doBeginPublish } from 'redux/actions/publish';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { doFetchItemsInCollection } from 'redux/actions/collections'; import { doFetchItemsInCollection } from 'redux/actions/collections';
@ -21,10 +22,12 @@ import { isStreamPlaceholderClaim } from 'util/claim';
import * as COLLECTIONS_CONSTS from 'constants/collections'; import * as COLLECTIONS_CONSTS from 'constants/collections';
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions'; import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
import { selectBlacklistedOutpointMap } from 'lbryinc'; import { selectBlacklistedOutpointMap } from 'lbryinc';
import { selectActiveLiveClaimForChannel } from 'redux/selectors/livestream';
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
import ShowPage from './view'; import ShowPage from './view';
const select = (state, props) => { const select = (state, props) => {
const { uri, location } = props; const { uri, location, liveContentPath } = props;
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
@ -35,9 +38,16 @@ const select = (state, props) => {
(claim && claim.value_type === 'collection' && claim.claim_id) || (claim && claim.value_type === 'collection' && claim.claim_id) ||
null; 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 { return {
uri, uri,
claim, claim,
latestClaimUrl,
isResolvingUri: selectIsUriResolving(state, uri), isResolvingUri: selectIsUriResolving(state, uri),
blackListedOutpointMap: selectBlacklistedOutpointMap(state), blackListedOutpointMap: selectBlacklistedOutpointMap(state),
isSubscribed: selectIsSubscribedForUri(state, uri), isSubscribed: selectIsSubscribedForUri(state, uri),
@ -58,6 +68,8 @@ const perform = {
doBeginPublish, doBeginPublish,
doFetchItemsInCollection, doFetchItemsInCollection,
doOpenModal, doOpenModal,
fetchLatestClaimForChannel: doFetchLatestClaimForChannel,
fetchChannelLiveStatus: doFetchChannelLiveStatus,
}; };
export default withRouter(connect(select, perform)(ShowPage)); export default withRouter(connect(select, perform)(ShowPage));

View file

@ -38,7 +38,12 @@ type Props = {
isResolvingCollection: boolean, isResolvingCollection: boolean,
isAuthenticated: boolean, isAuthenticated: boolean,
geoRestriction: ?GeoRestriction, 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, doBeginPublish: (name: ?string) => void,
doFetchItemsInCollection: ({ collectionId: string }) => void, doFetchItemsInCollection: ({ collectionId: string }) => void,
doOpenModal: (string, {}) => void, doOpenModal: (string, {}) => void,
@ -61,6 +66,11 @@ export default function ShowPage(props: Props) {
isResolvingCollection, isResolvingCollection,
isAuthenticated, isAuthenticated,
geoRestriction, geoRestriction,
latestContentPath,
liveContentPath,
latestClaimUrl,
fetchLatestClaimForChannel,
fetchChannelLiveStatus,
doResolveUri, doResolveUri,
doBeginPublish, doBeginPublish,
doFetchItemsInCollection, doFetchItemsInCollection,
@ -77,6 +87,8 @@ export default function ShowPage(props: Props) {
const claimExists = claim !== null && claim !== undefined; const claimExists = claim !== null && claim !== undefined;
const haventFetchedYet = claim === undefined; const haventFetchedYet = claim === undefined;
const isMine = claim && claim.is_my_output; 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 { contentName, isChannel } = parseURI(uri); // deprecated contentName - use streamName and channelName
const isCollection = claim && claim.value_type === 'collection'; const isCollection = claim && claim.value_type === 'collection';
@ -90,6 +102,26 @@ export default function ShowPage(props: Props) {
blackListedOutpointMap[`${claim.txid}:${claim.nout}`] 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. // changed this from 'isCollection' to resolve strangers' collections.
useEffect(() => { useEffect(() => {
if (collectionId && !resolvedCollection) { if (collectionId && !resolvedCollection) {
@ -150,9 +182,24 @@ export default function ShowPage(props: Props) {
isAuthenticated, 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 // Don't navigate directly to repost urls
// Always redirect to the actual content // 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); const newUrl = formatLbryUrlForWeb(canonicalUrl);
return <Redirect to={newUrl} />; return <Redirect to={newUrl} />;
} }

View file

@ -1134,3 +1134,23 @@ export const doCheckPendingClaims = (onChannelConfirmed: Function) => (dispatch:
checkTxoList(); checkTxoList();
}, 30000); }, 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 }));
};

View file

@ -61,6 +61,7 @@ type State = {
isCheckingNameForPublish: boolean, isCheckingNameForPublish: boolean,
checkingPending: boolean, checkingPending: boolean,
checkingReflecting: boolean, checkingReflecting: boolean,
latestByUri: { [string]: any },
}; };
const reducers = {}; const reducers = {};
@ -109,6 +110,7 @@ const defaultState = {
isCheckingNameForPublish: false, isCheckingNameForPublish: false,
checkingPending: false, checkingPending: false,
checkingReflecting: 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 => { reducers[ACTIONS.PURCHASE_LIST_COMPLETED] = (state: State, action: any): State => {
const { result }: { result: PurchaseListResponse, resolve: boolean } = action.data; const { result }: { result: PurchaseListResponse, resolve: boolean } = action.data;
const page = result.page; const page = result.page;

View file

@ -7,7 +7,7 @@ const defaultState: LivestreamState = {
fetchingById: {}, fetchingById: {},
viewersById: {}, viewersById: {},
fetchingActiveLivestreams: 'pending', fetchingActiveLivestreams: 'pending',
activeLivestreams: null, activeLivestreams: {},
activeLivestreamsLastFetchedDate: 0, activeLivestreamsLastFetchedDate: 0,
activeLivestreamsLastFetchedOptions: {}, activeLivestreamsLastFetchedOptions: {},
activeLivestreamsLastFetchedFailCount: 0, activeLivestreamsLastFetchedFailCount: 0,
@ -95,9 +95,9 @@ export default handleActions(
}; };
}, },
[ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS]: (state: LivestreamState, action: any) => { [ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS]: (state: LivestreamState, action: any) => {
const activeLivestreams = state.activeLivestreams; const activeLivestreams = Object.assign({}, state.activeLivestreams);
if (activeLivestreams) delete activeLivestreams[action.data.channelId]; activeLivestreams[action.data.channelId] = null;
return { ...state, activeLivestreams: Object.assign({}, activeLivestreams), activeLivestreamInitialized: true }; return { ...state, activeLivestreams, activeLivestreamInitialized: true };
}, },
[ACTIONS.SOCKET_CONNECTED_BY_ID]: (state: LivestreamState, action: any) => { [ACTIONS.SOCKET_CONNECTED_BY_ID]: (state: LivestreamState, action: any) => {
const { connected, sub_category, id: claimId } = action.data; const { connected, sub_category, id: claimId } = action.data;

View file

@ -33,6 +33,17 @@ export const selectCreatingChannel = (state: State) => selectState(state).creati
export const selectCreateChannelError = (state: State) => selectState(state).createChannelError; export const selectCreateChannelError = (state: State) => selectState(state).createChannelError;
export const selectRepostLoading = (state: State) => selectState(state).repostLoading; export const selectRepostLoading = (state: State) => selectState(state).repostLoading;
export const selectRepostError = (state: State) => selectState(state).repostError; 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) => { export const selectClaimsByUri = createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => {
const claims = {}; const claims = {};
@ -810,7 +821,7 @@ export const selectIsMyChannelCountOverLimit = createSelector(
* @param uri * @param uri
* @returns {*} * @returns {*}
*/ */
export const selectOdyseeMembershipForUri = function (state: State, uri: string) { export const selectOdyseeMembershipForUri = (state: State, uri: string) => {
const claim = selectClaimForUri(state, uri); const claim = selectClaimForUri(state, uri);
const uploaderChannelClaimId = getChannelIdFromClaim(claim); const uploaderChannelClaimId = getChannelIdFromClaim(claim);
@ -834,7 +845,7 @@ export const selectOdyseeMembershipForUri = function (state: State, uri: string)
* @param channelId * @param channelId
* @returns {*} * @returns {*}
*/ */
export const selectOdyseeMembershipForChannelId = function (state: State, channelId: string) { export const selectOdyseeMembershipForChannelId = (state: State, channelId: string) => {
// looks for the uploader id // looks for the uploader id
const matchingMembershipOfUser = const matchingMembershipOfUser =
state.user && state.user.odyseeMembershipsPerClaimIds && state.user.odyseeMembershipsPerClaimIds[channelId]; state.user && state.user.odyseeMembershipsPerClaimIds && state.user.odyseeMembershipsPerClaimIds[channelId];

View file

@ -1,7 +1,7 @@
// @flow // @flow
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { createCachedSelector } from 're-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 }; type State = { livestream: any };
@ -91,6 +91,12 @@ export const selectActiveLivestreamForChannel = createCachedSelector(
if (!channelId || !activeLivestreams) { if (!channelId || !activeLivestreams) {
return null; return null;
} }
return activeLivestreams[channelId] || null; return activeLivestreams[channelId];
} }
)((state, channelId) => String(channelId)); )((state, channelId) => String(channelId));
export const selectActiveLiveClaimForChannel = createCachedSelector(
(state) => state,
selectActiveLivestreamForChannel,
(state, activeLivestream) => activeLivestream && selectClaimForUri(state, activeLivestream.claimUri)
)((state, channelId) => String(channelId));