Merge branch 'master' into protocol
This commit is contained in:
commit
429528608e
34 changed files with 376 additions and 99 deletions
|
@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Add watch later to hover action for last used playlist on popup _community pr!_ ([#6274](https://github.com/lbryio/lbry-desktop/pull/6274))
|
||||
- Open in desktop (web feature) _community pr!_ ([#6667](https://github.com/lbryio/lbry-desktop/pull/6667))
|
||||
- Add confirmation on comment removal _community pr!_ ([#6563](https://github.com/lbryio/lbry-desktop/pull/6563))
|
||||
- Show on content page if a file is part of a playlist already _community pr!_([#6393](https://github.com/lbryio/lbry-desktop/pull/6393))
|
||||
|
||||
### Changed
|
||||
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#a327385cdf71568dbd15a17f3dcf5f4b83e0966d",
|
||||
"lbry-redux": "lbryio/lbry-redux#7cc9923ed9ff1940b508842af6be44c8da906a60",
|
||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -209,6 +209,7 @@
|
|||
"Your comment": "Your comment",
|
||||
"Post --[button to submit something]--": "Post",
|
||||
"Post --[noun, markdown post tab button]--": "Post",
|
||||
"Livestream --[noun, livestream tab button]--": "Livestream --[noun, livestream tab button]--",
|
||||
"Posting...": "Posting...",
|
||||
"Incompatible daemon": "Incompatible daemon",
|
||||
"Incompatible daemon running": "Incompatible daemon running",
|
||||
|
@ -2066,5 +2067,10 @@
|
|||
"Open in Desktop": "Open in Desktop",
|
||||
"Show %count% replies": "Show %count% replies",
|
||||
"Show reply": "Show reply",
|
||||
"Confirm Comment Deletion": "Confirm Comment Deletion",
|
||||
"Remove Comment": "Remove Comment",
|
||||
"Are you sure you want to remove this comment?": "Are you sure you want to remove this comment?",
|
||||
"This comment has a tip associated with it which cannot be reverted.": "This comment has a tip associated with it which cannot be reverted.",
|
||||
"Recent Comments": "Recent Comments",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import CollectionAddButton from './view';
|
||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { makeSelectClaimForUri, makeSelectClaimUrlInCollection } from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
|
||||
return {
|
||||
claim,
|
||||
isSaved: makeSelectClaimUrlInCollection(permanentUrl)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, {
|
||||
doOpenModal,
|
||||
|
|
|
@ -11,10 +11,11 @@ type Props = {
|
|||
fileAction?: boolean,
|
||||
type?: boolean,
|
||||
claim: Claim,
|
||||
isSaved: boolean,
|
||||
};
|
||||
|
||||
export default function CollectionAddButton(props: Props) {
|
||||
const { doOpenModal, uri, fileAction, type = 'playlist', claim } = props;
|
||||
const { doOpenModal, uri, fileAction, type = 'playlist', claim, isSaved } = props;
|
||||
|
||||
// $FlowFixMe
|
||||
const streamType = (claim && claim.value && claim.value.stream_type) || '';
|
||||
|
@ -25,9 +26,9 @@ export default function CollectionAddButton(props: Props) {
|
|||
<Button
|
||||
button={fileAction ? undefined : 'alt'}
|
||||
className={classnames({ 'button--file-action': fileAction })}
|
||||
icon={fileAction ? ICONS.ADD : ICONS.LIBRARY}
|
||||
icon={fileAction ? (!isSaved ? ICONS.ADD : ICONS.STACK) : ICONS.LIBRARY}
|
||||
iconSize={fileAction ? 22 : undefined}
|
||||
label={uri ? __('Save') : __('New List')}
|
||||
label={uri ? (!isSaved ? __('Save') : __('Saved')) : __('New List')}
|
||||
requiresAuth={IS_WEB}
|
||||
title={__('Add this claim to a list')}
|
||||
onClick={(e) => {
|
||||
|
|
|
@ -47,6 +47,7 @@ type Props = {
|
|||
searchOptions?: any,
|
||||
collectionId?: string,
|
||||
showNoSourceClaims?: boolean,
|
||||
onClick?: (e: any, index?: number) => void,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
|
@ -55,6 +56,7 @@ export default function ClaimList(props: Props) {
|
|||
uris,
|
||||
headerAltControls,
|
||||
loading,
|
||||
id,
|
||||
persistedStorageKey,
|
||||
empty,
|
||||
defaultSort,
|
||||
|
@ -80,6 +82,7 @@ export default function ClaimList(props: Props) {
|
|||
searchOptions,
|
||||
collectionId,
|
||||
showNoSourceClaims,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
|
@ -107,6 +110,12 @@ export default function ClaimList(props: Props) {
|
|||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||
}
|
||||
|
||||
function handleClaimClicked(e, index) {
|
||||
if (onClick) {
|
||||
onClick(e, index);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = debounce((e) => {
|
||||
if (page && pageSize && onScrollBottom) {
|
||||
|
@ -191,6 +200,8 @@ export default function ClaimList(props: Props) {
|
|||
{injectedItem && index === 4 && <li>{injectedItem}</li>}
|
||||
<ClaimPreview
|
||||
uri={uri}
|
||||
containerId={id}
|
||||
indexInContainer={index}
|
||||
type={type}
|
||||
active={activeUri && uri === activeUri}
|
||||
hideMenu={hideMenu}
|
||||
|
@ -209,6 +220,7 @@ export default function ClaimList(props: Props) {
|
|||
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
|
||||
}}
|
||||
live={resolveLive(index)}
|
||||
onClick={handleClaimClicked}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
|
@ -106,7 +106,7 @@ function ClaimListDiscover(props: Props) {
|
|||
claimType,
|
||||
pageSize,
|
||||
defaultClaimType,
|
||||
streamType = SIMPLE_SITE ? CS.FILE_VIDEO : undefined,
|
||||
streamType = SIMPLE_SITE ? [CS.FILE_VIDEO, CS.FILE_AUDIO] : undefined,
|
||||
defaultStreamType = SIMPLE_SITE ? CS.FILE_VIDEO : undefined, // add param for DEFAULT_STREAM_TYPE
|
||||
freshness,
|
||||
defaultFreshness = CS.FRESH_WEEK,
|
||||
|
@ -319,7 +319,7 @@ function ClaimListDiscover(props: Props) {
|
|||
}
|
||||
|
||||
if (streamTypeParam && streamTypeParam !== CS.CONTENT_ALL && claimType !== CS.CLAIM_CHANNEL) {
|
||||
options.stream_types = [streamTypeParam];
|
||||
options.stream_types = typeof streamTypeParam === 'string' ? [streamTypeParam] : streamTypeParam;
|
||||
}
|
||||
|
||||
if (claimTypeParam) {
|
||||
|
|
|
@ -400,6 +400,13 @@ function ClaimMenuList(props: Props) {
|
|||
)}
|
||||
<hr className="menu__separator" />
|
||||
|
||||
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.COPY_LINK} />
|
||||
{__('Copy Link')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
{isChannelPage && IS_WEB && rssUrl && (
|
||||
<MenuItem className="comment__menu-option" onSelect={handleCopyRssLink}>
|
||||
<div className="menu__link">
|
||||
|
@ -417,14 +424,7 @@ function ClaimMenuList(props: Props) {
|
|||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.SHARE} />
|
||||
{__('Copy Link')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
|
||||
{!claimIsMine && !isMyCollection && (
|
||||
<MenuItem className="comment__menu-option" onSelect={handleReportContent}>
|
||||
<div className="menu__link">
|
||||
|
|
|
@ -29,6 +29,7 @@ import ClaimPreviewNoContent from './claim-preview-no-content';
|
|||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { CONTAINER_ID } from 'constants/navigation';
|
||||
|
||||
const AbandonedChannelPreview = lazyImport(() =>
|
||||
import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */)
|
||||
|
@ -45,7 +46,7 @@ type Props = {
|
|||
reflectingProgress?: any, // fxme
|
||||
resolveUri: (string) => void,
|
||||
isResolvingUri: boolean,
|
||||
history: { push: (string) => void },
|
||||
history: { push: (string | any) => void },
|
||||
title: string,
|
||||
nsfw: boolean,
|
||||
placeholder: string,
|
||||
|
@ -65,7 +66,7 @@ type Props = {
|
|||
actions: boolean | Node | string | number,
|
||||
properties: boolean | Node | string | number | ((Claim) => Node),
|
||||
empty?: Node,
|
||||
onClick?: (any) => any,
|
||||
onClick?: (e: any, index?: number) => any,
|
||||
streamingUrl: ?string,
|
||||
getFile: (string) => void,
|
||||
customShouldHide?: (Claim) => boolean,
|
||||
|
@ -88,6 +89,8 @@ type Props = {
|
|||
disableNavigation?: boolean,
|
||||
mediaDuration?: string,
|
||||
date?: any,
|
||||
containerId?: string, // ID or name of the container (e.g. ClaimList, HOC, etc.) that this is in.
|
||||
indexInContainer?: number, // The index order of this component within 'containerId'.
|
||||
};
|
||||
|
||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
|
@ -149,6 +152,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
isCollectionMine,
|
||||
collectionUris,
|
||||
disableNavigation,
|
||||
containerId,
|
||||
indexInContainer,
|
||||
} = props;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const collectionClaimId = isCollection && claim && claim.claim_id;
|
||||
|
@ -202,9 +207,21 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
}
|
||||
|
||||
const handleNavLinkClick = (e) => {
|
||||
if (onClick) {
|
||||
onClick(e, indexInContainer);
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const navLinkProps = {
|
||||
to: navigateUrl,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
to: {
|
||||
pathname: navigateUrl,
|
||||
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
|
||||
},
|
||||
onClick: (e) => handleNavLinkClick(e),
|
||||
onAuxClick: (e) => handleNavLinkClick(e),
|
||||
};
|
||||
|
||||
// do not block abandoned and nsfw claims if showUserBlocked is passed
|
||||
|
@ -250,11 +267,14 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
|
||||
function handleOnClick(e) {
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
onClick(e, indexInContainer);
|
||||
}
|
||||
|
||||
if (claim && !pending && !disableNavigation) {
|
||||
history.push(navigateUrl);
|
||||
history.push({
|
||||
pathname: navigateUrl,
|
||||
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -159,6 +159,12 @@ function ClaimTilesDiscover(props: Props) {
|
|||
new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1]))
|
||||
);
|
||||
const liveUris = [];
|
||||
let streamTypesParam;
|
||||
if (streamTypes) {
|
||||
streamTypesParam = streamTypes;
|
||||
} else if (SIMPLE_SITE && !hasNoSource && streamTypes !== null) {
|
||||
streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO];
|
||||
}
|
||||
|
||||
const [prevUris, setPrevUris] = React.useState([]);
|
||||
|
||||
|
@ -192,8 +198,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
channel_ids: channelIds || [],
|
||||
not_channel_ids: mutedAndBlockedChannelIds,
|
||||
order_by: orderBy || ['trending_group', 'trending_mixed'],
|
||||
stream_types:
|
||||
streamTypes === null ? undefined : SIMPLE_SITE && !hasNoSource ? [CS.FILE_VIDEO, CS.FILE_AUDIO] : undefined,
|
||||
stream_types: streamTypesParam,
|
||||
};
|
||||
|
||||
if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import usePersistedState from 'effects/use-persisted-state';
|
|||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import Empty from 'component/common/empty';
|
||||
import debounce from 'util/debounce';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||
|
||||
|
@ -74,6 +75,8 @@ function CommentList(props: Props) {
|
|||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||
const [page, setPage] = React.useState(0);
|
||||
const isMobile = useIsMobile();
|
||||
const [expandedComments, setExpandedComments] = React.useState(!isMobile);
|
||||
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||
|
||||
// Display comments immediately if not fetching reactions
|
||||
|
@ -191,7 +194,7 @@ function CommentList(props: Props) {
|
|||
}
|
||||
|
||||
const handleCommentScroll = debounce(() => {
|
||||
if (shouldFetchNextPage(page, topLevelTotalPages, window, document)) {
|
||||
if (!isMobile && shouldFetchNextPage(page, topLevelTotalPages, window, document)) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
||||
|
@ -205,6 +208,7 @@ function CommentList(props: Props) {
|
|||
}
|
||||
}
|
||||
}, [
|
||||
isMobile,
|
||||
page,
|
||||
moreBelow,
|
||||
spinnerRef,
|
||||
|
@ -279,7 +283,13 @@ function CommentList(props: Props) {
|
|||
<Empty padded text={__('That was pretty deep. What do you think?')} />
|
||||
)}
|
||||
|
||||
<ul className="comments" ref={commentRef}>
|
||||
<ul
|
||||
className={classnames({
|
||||
comments: expandedComments,
|
||||
'comments--contracted': !expandedComments,
|
||||
})}
|
||||
ref={commentRef}
|
||||
>
|
||||
{topLevelComments &&
|
||||
displayedComments &&
|
||||
displayedComments.map((comment) => {
|
||||
|
@ -307,7 +317,36 @@ function CommentList(props: Props) {
|
|||
})}
|
||||
</ul>
|
||||
|
||||
{(isFetchingComments || moreBelow) && (
|
||||
{isMobile && (
|
||||
<div className="card__bottom-actions--comments">
|
||||
{moreBelow && (
|
||||
<Button
|
||||
button="link"
|
||||
title={!expandedComments ? __('Expand Comments') : __('Load More')}
|
||||
label={!expandedComments ? __('Expand') : __('More')}
|
||||
onClick={() => {
|
||||
if (!expandedComments) {
|
||||
setExpandedComments(true);
|
||||
} else {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{expandedComments && (
|
||||
<Button
|
||||
button="link"
|
||||
title={__('Collapse Thread')}
|
||||
label={__('Collapse')}
|
||||
onClick={() => {
|
||||
setExpandedComments(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isFetchingComments || (!isMobile && moreBelow)) && (
|
||||
<div className="main--empty" ref={spinnerRef}>
|
||||
<Spinner type="small" />
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,17 @@ export default function Logo(props: Props) {
|
|||
if (LOGO_TEXT_LIGHT) {
|
||||
return (
|
||||
<>
|
||||
<img className={'header__navigation-logo'} src={LOGO_TEXT_LIGHT} />
|
||||
<img className={'embed__overlay-logo'} src={LOGO_TEXT_LIGHT} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return defaultWithLabel;
|
||||
}
|
||||
} else if (type === 'embed-ended') {
|
||||
if (LOGO_TEXT_LIGHT) {
|
||||
return (
|
||||
<>
|
||||
<img className={'embed__overlay-logo'} src={LOGO_TEXT_LIGHT} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimIsNsfw, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { doRecommendationUpdate, doRecommendationClicked } from 'redux/actions/content';
|
||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||
import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
@ -17,6 +18,9 @@ const select = (state, props) => ({
|
|||
|
||||
const perform = (dispatch) => ({
|
||||
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)),
|
||||
doRecommendationUpdate: (claimId, urls, id, parentId) =>
|
||||
dispatch(doRecommendationUpdate(claimId, urls, id, parentId)),
|
||||
doRecommendationClicked: (claimId, index) => dispatch(doRecommendationClicked(claimId, index)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(RecommendedContent);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// @flow
|
||||
import { SHOW_ADS } from 'config';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import ClaimList from 'component/claimList';
|
||||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import Ads from 'web/component/ads';
|
||||
|
@ -8,6 +10,7 @@ import Card from 'component/common/card';
|
|||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import { CONTAINER_ID } from 'constants/navigation';
|
||||
|
||||
const VIEW_ALL_RELATED = 'view_all_related';
|
||||
const VIEW_MORE_FROM = 'view_more_from';
|
||||
|
@ -21,6 +24,8 @@ type Props = {
|
|||
mature: boolean,
|
||||
isAuthenticated: boolean,
|
||||
claim: ?StreamClaim,
|
||||
doRecommendationUpdate: (claimId: string, urls: Array<string>, id: string, parentId: string) => void,
|
||||
doRecommendationClicked: (claimId: string, index: number) => void,
|
||||
};
|
||||
|
||||
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||
|
@ -33,33 +38,70 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
|||
isSearching,
|
||||
isAuthenticated,
|
||||
claim,
|
||||
doRecommendationUpdate,
|
||||
doRecommendationClicked,
|
||||
} = props;
|
||||
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
||||
const [recommendationId, setRecommendationId] = React.useState('');
|
||||
const [recommendationUrls, setRecommendationUrls] = React.useState();
|
||||
const history = useHistory();
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
const channelName = signingChannel ? signingChannel.name : null;
|
||||
const isMobile = useIsMobile();
|
||||
const isMedium = useIsMediumScreen();
|
||||
|
||||
function reorderList(recommendedContent) {
|
||||
let newList = recommendedContent;
|
||||
if (newList) {
|
||||
const index = newList.indexOf(nextRecommendedUri);
|
||||
if (index === -1) {
|
||||
// This would be weird. Shouldn't happen since it is derived from the same list.
|
||||
} else if (index !== 0) {
|
||||
// Swap the "next" item to the top of the list
|
||||
const a = newList[0];
|
||||
newList[0] = nextRecommendedUri;
|
||||
newList[index] = a;
|
||||
React.useEffect(() => {
|
||||
function moveAutoplayNextItemToTop(recommendedContent) {
|
||||
let newList = recommendedContent;
|
||||
if (newList) {
|
||||
const index = newList.indexOf(nextRecommendedUri);
|
||||
if (index > 0) {
|
||||
const a = newList[0];
|
||||
newList[0] = nextRecommendedUri;
|
||||
newList[index] = a;
|
||||
}
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
|
||||
function listEq(prev, next) {
|
||||
if (prev && next) {
|
||||
return prev.length === next.length && prev.every((value, index) => value === next[index]);
|
||||
} else {
|
||||
return prev === next;
|
||||
}
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
|
||||
const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContent);
|
||||
|
||||
if (claim && !listEq(recommendationUrls, newRecommendationUrls)) {
|
||||
const parentId = (history.location.state && history.location.state[CONTAINER_ID]) || '';
|
||||
const id = uuidv4();
|
||||
setRecommendationId(id);
|
||||
setRecommendationUrls(newRecommendationUrls);
|
||||
|
||||
doRecommendationUpdate(claim.claim_id, newRecommendationUrls, id, parentId);
|
||||
}
|
||||
}, [
|
||||
recommendedContent,
|
||||
nextRecommendedUri,
|
||||
recommendationUrls,
|
||||
setRecommendationUrls,
|
||||
claim,
|
||||
doRecommendationUpdate,
|
||||
history.location.state,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
doFetchRecommendedContent(uri, mature);
|
||||
}, [uri, mature, doFetchRecommendedContent]);
|
||||
|
||||
function handleRecommendationClicked(e: any, index: number) {
|
||||
if (claim) {
|
||||
doRecommendationClicked(claim.claim_id, index);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
isBodyList
|
||||
|
@ -91,12 +133,14 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
|||
<div>
|
||||
{viewMode === VIEW_ALL_RELATED && (
|
||||
<ClaimList
|
||||
id={recommendationId}
|
||||
type="small"
|
||||
loading={isSearching}
|
||||
uris={reorderList(recommendedContent)}
|
||||
uris={recommendationUrls}
|
||||
hideMenu={isMobile}
|
||||
injectedItem={SHOW_ADS && IS_WEB && !isAuthenticated && <Ads small type={'video'} />}
|
||||
empty={__('No related content found')}
|
||||
onClick={handleRecommendationClicked}
|
||||
/>
|
||||
)}
|
||||
{viewMode === VIEW_MORE_FROM && signingChannel && (
|
||||
|
|
|
@ -7,7 +7,7 @@ import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
|||
import VideoViewer from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||
import { selectDaemonSettings, makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||
import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
|
||||
|
@ -31,6 +31,7 @@ const select = (state, props) => {
|
|||
homepageData: selectHomepageData(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
userId: userId,
|
||||
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
// Created by xander on 6/21/2021
|
||||
import videojs from 'video.js';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import {
|
||||
makeSelectRecommendationId,
|
||||
makeSelectRecommendationParentId,
|
||||
makeSelectRecommendedClaimIds,
|
||||
makeSelectRecommendationClicks,
|
||||
} from 'redux/selectors/content';
|
||||
|
||||
const VERSION = '0.0.1';
|
||||
|
||||
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
|
||||
|
@ -17,23 +23,28 @@ const RecsysData = {
|
|||
};
|
||||
|
||||
function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
|
||||
// TODO: use a UUID generator
|
||||
const uuid = uuidV4();
|
||||
const pageLoadedAt = loadedAt;
|
||||
const pageExitedAt = Date.now();
|
||||
return {
|
||||
uuid: uuid,
|
||||
parentUuid: null,
|
||||
uid: userId,
|
||||
claimId: claimId,
|
||||
pageLoadedAt: pageLoadedAt,
|
||||
pageExitedAt: pageExitedAt,
|
||||
recsysId: recsysId,
|
||||
recClaimIds: null,
|
||||
recClickedVideoIdx: null,
|
||||
events: events,
|
||||
isEmbed: isEmbed,
|
||||
};
|
||||
|
||||
if (window.store) {
|
||||
const state = window.store.getState();
|
||||
|
||||
return {
|
||||
uuid: makeSelectRecommendationId(claimId)(state),
|
||||
parentUuid: makeSelectRecommendationParentId(claimId)(state),
|
||||
uid: userId,
|
||||
claimId: claimId,
|
||||
pageLoadedAt: pageLoadedAt,
|
||||
pageExitedAt: pageExitedAt,
|
||||
recsysId: recsysId,
|
||||
recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
|
||||
recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
|
||||
events: events,
|
||||
isEmbed: isEmbed,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function newRecsysEvent(eventType, offset, arg) {
|
||||
|
@ -130,7 +141,10 @@ class RecsysPlugin extends Component {
|
|||
this.loadedAt,
|
||||
false
|
||||
);
|
||||
sendRecsysEvents(event);
|
||||
|
||||
if (event) {
|
||||
sendRecsysEvents(event);
|
||||
}
|
||||
}
|
||||
|
||||
onPlay(event) {
|
||||
|
@ -226,7 +240,7 @@ const onPlayerReady = (player, options) => {
|
|||
* @function plugin
|
||||
* @param {Object} [options={}]
|
||||
*/
|
||||
const plugin = function(options) {
|
||||
const plugin = function (options) {
|
||||
this.ready(() => {
|
||||
onPlayerReady(this, videojs.mergeOptions(defaults, options));
|
||||
});
|
||||
|
|
|
@ -56,6 +56,7 @@ type Props = {
|
|||
claimId: ?string,
|
||||
userId: ?number,
|
||||
allowPreRoll: ?boolean,
|
||||
shareTelemetry: boolean,
|
||||
};
|
||||
|
||||
// type VideoJSOptions = {
|
||||
|
@ -193,6 +194,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
claimId,
|
||||
userId,
|
||||
// allowPreRoll,
|
||||
shareTelemetry,
|
||||
} = props;
|
||||
|
||||
const [reload, setReload] = useState('initial');
|
||||
|
@ -564,11 +566,12 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
});
|
||||
|
||||
// Add recsys plugin
|
||||
// TODO: Add an if(odysee.com) around this function to only use recsys on odysee
|
||||
player.recsys({
|
||||
videoId: claimId,
|
||||
userId: userId,
|
||||
});
|
||||
if (shareTelemetry) {
|
||||
player.recsys({
|
||||
videoId: claimId,
|
||||
userId: userId,
|
||||
});
|
||||
}
|
||||
|
||||
// set playsinline for mobile
|
||||
// TODO: make this better
|
||||
|
|
|
@ -51,6 +51,7 @@ type Props = {
|
|||
authenticated: boolean,
|
||||
userId: number,
|
||||
homepageData?: { [string]: HomepageCat },
|
||||
shareTelemetry: boolean,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -84,6 +85,7 @@ function VideoViewer(props: Props) {
|
|||
homepageData,
|
||||
authenticated,
|
||||
userId,
|
||||
shareTelemetry,
|
||||
} = props;
|
||||
|
||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||
|
@ -319,6 +321,7 @@ function VideoViewer(props: Props) {
|
|||
claimId={claimId}
|
||||
userId={userId}
|
||||
allowPreRoll={!embedded && !authenticated}
|
||||
shareTelemetry={shareTelemetry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -98,6 +98,8 @@ export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
|
|||
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
||||
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
||||
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
||||
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
||||
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
||||
|
||||
// Files
|
||||
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
|
||||
|
|
1
ui/constants/navigation.js
Normal file
1
ui/constants/navigation.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const CONTAINER_ID = 'CONTAINER_ID';
|
|
@ -125,9 +125,11 @@ function DiscoverPage(props: Props) {
|
|||
// Assume wild west page if no dynamicRouteProps
|
||||
// Not a very good solution, but just doing it for now
|
||||
// until we are sure this page will stay around
|
||||
// TODO: find a better way to determine discover / wild west vs other modes release times
|
||||
// for now including && !tags so that
|
||||
releaseTime={
|
||||
SIMPLE_SITE
|
||||
? !dynamicRouteProps && `>${Math.floor(moment().subtract(1, 'day').startOf('week').unix())}`
|
||||
? !dynamicRouteProps && !tags && `>${Math.floor(moment().subtract(1, 'day').startOf('week').unix())}`
|
||||
: undefined
|
||||
}
|
||||
feeAmount={SIMPLE_SITE ? !dynamicRouteProps && CS.FEE_AMOUNT_ANY : undefined}
|
||||
|
|
|
@ -40,7 +40,9 @@ function TopPage(props: Props) {
|
|||
label={__('Repost Here')}
|
||||
/>
|
||||
),
|
||||
publish: <Button button="secondary" onClick={() => beginPublish(queryName)} label={'Publish Here'} />,
|
||||
publish: (
|
||||
<Button button="secondary" onClick={() => beginPublish(queryName)} label={__('Publish Here')} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
%repost% %publish%
|
||||
|
|
|
@ -43,7 +43,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
|
|||
full_status: true,
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
}).then(result => {
|
||||
}).then((result) => {
|
||||
const { items: fileInfos } = result;
|
||||
const fileInfo = fileInfos[0];
|
||||
if (!fileInfo || fileInfo.written_bytes === 0) {
|
||||
|
@ -261,3 +261,21 @@ export function doClearContentHistoryAll() {
|
|||
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL });
|
||||
};
|
||||
}
|
||||
|
||||
export const doRecommendationUpdate = (claimId: string, urls: Array<string>, id: string, parentId: string) => (
|
||||
dispatch: Dispatch
|
||||
) => {
|
||||
dispatch({
|
||||
type: ACTIONS.RECOMMENDATION_UPDATED,
|
||||
data: { claimId, urls, id, parentId },
|
||||
});
|
||||
};
|
||||
|
||||
export const doRecommendationClicked = (claimId: string, index: number) => (dispatch: Dispatch) => {
|
||||
if (index !== undefined && index !== null) {
|
||||
dispatch({
|
||||
type: ACTIONS.RECOMMENDATION_CLICKED,
|
||||
data: { claimId, index },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,6 +7,10 @@ const defaultState = {
|
|||
channelClaimCounts: {},
|
||||
positions: {},
|
||||
history: [],
|
||||
recommendationId: {}, // { "claimId": "recommendationId" }
|
||||
recommendationParentId: {}, // { "claimId": "referrerId" }
|
||||
recommendationUrls: {}, // { "claimId": [lbryUrls...] }
|
||||
recommendationClicks: {}, // { "claimId": [clicked indices...] }
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
|
||||
|
@ -73,7 +77,7 @@ reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => {
|
|||
const { uri, lastViewed } = action.data;
|
||||
const { history } = state;
|
||||
const historyObj = { uri, lastViewed };
|
||||
const index = history.findIndex(i => i.uri === uri);
|
||||
const index = history.findIndex((i) => i.uri === uri);
|
||||
const newHistory =
|
||||
index === -1
|
||||
? [historyObj].concat(history)
|
||||
|
@ -84,7 +88,7 @@ reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => {
|
|||
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
|
||||
const { uri } = action.data;
|
||||
const { history } = state;
|
||||
const index = history.findIndex(i => i.uri === uri);
|
||||
const index = history.findIndex((i) => i.uri === uri);
|
||||
return index === -1
|
||||
? state
|
||||
: {
|
||||
|
@ -93,7 +97,44 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
|
|||
};
|
||||
};
|
||||
|
||||
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = state => ({ ...state, history: [] });
|
||||
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] });
|
||||
|
||||
reducers[ACTIONS.RECOMMENDATION_UPDATED] = (state, action) => {
|
||||
const { claimId, urls, id, parentId } = action.data;
|
||||
const recommendationId = Object.assign({}, state.recommendationId);
|
||||
const recommendationParentId = Object.assign({}, state.recommendationParentId);
|
||||
const recommendationUrls = Object.assign({}, state.recommendationUrls);
|
||||
const recommendationClicks = Object.assign({}, state.recommendationClicks);
|
||||
|
||||
if (urls && urls.length > 0) {
|
||||
recommendationId[claimId] = id;
|
||||
recommendationParentId[claimId] = parentId;
|
||||
recommendationUrls[claimId] = urls;
|
||||
recommendationClicks[claimId] = [];
|
||||
} else {
|
||||
delete recommendationId[claimId];
|
||||
delete recommendationParentId[claimId];
|
||||
delete recommendationUrls[claimId];
|
||||
delete recommendationClicks[claimId];
|
||||
}
|
||||
|
||||
return { ...state, recommendationId, recommendationParentId, recommendationUrls, recommendationClicks };
|
||||
};
|
||||
|
||||
reducers[ACTIONS.RECOMMENDATION_CLICKED] = (state, action) => {
|
||||
const { claimId, index } = action.data;
|
||||
const recommendationClicks = Object.assign({}, state.recommendationClicks);
|
||||
|
||||
if (state.recommendationUrls[claimId] && index >= 0 && index < state.recommendationUrls[claimId].length) {
|
||||
if (recommendationClicks[claimId]) {
|
||||
recommendationClicks[claimId].push(index);
|
||||
} else {
|
||||
recommendationClicks[claimId] = [index];
|
||||
}
|
||||
}
|
||||
|
||||
return { ...state, recommendationClicks };
|
||||
};
|
||||
|
||||
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||
// return {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
makeSelectFileNameForUri,
|
||||
normalizeURI,
|
||||
selectMyActiveClaims,
|
||||
selectClaimIdsByUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
|
@ -247,19 +248,37 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) =>
|
|||
);
|
||||
|
||||
export const makeSelectSigningIsMine = (rawUri: string) => {
|
||||
let uri;
|
||||
let uri;
|
||||
try {
|
||||
uri = normalizeURI(rawUri);
|
||||
} catch (e) {}
|
||||
|
||||
return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
|
||||
try {
|
||||
uri = normalizeURI(rawUri);
|
||||
} catch (e) { }
|
||||
parseURI(uri);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
const signingChannel = claims && claims[uri] && (claims[uri].signing_channel || claims[uri]);
|
||||
|
||||
return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
|
||||
try {
|
||||
parseURI(uri);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
const signingChannel = claims && claims[uri] && (claims[uri].signing_channel || claims[uri]);
|
||||
return signingChannel && myClaims.has(signingChannel.claim_id);
|
||||
});
|
||||
};
|
||||
|
||||
return signingChannel && myClaims.has(signingChannel.claim_id);
|
||||
});
|
||||
};
|
||||
export const makeSelectRecommendationId = (claimId: string) =>
|
||||
createSelector(selectState, (state) => state.recommendationId[claimId]);
|
||||
|
||||
export const makeSelectRecommendationParentId = (claimId: string) =>
|
||||
createSelector(selectState, (state) => state.recommendationParentId[claimId]);
|
||||
|
||||
export const makeSelectRecommendedClaimIds = (claimId: string) =>
|
||||
createSelector(selectState, selectClaimIdsByUri, (state, claimIdsByUri) => {
|
||||
const recommendationUrls = state.recommendationUrls[claimId];
|
||||
if (recommendationUrls) {
|
||||
return recommendationUrls.map((url) => claimIdsByUri[url]);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export const makeSelectRecommendationClicks = (claimId: string) =>
|
||||
createSelector(selectState, (state) => state.recommendationClicks[claimId]);
|
||||
|
|
|
@ -391,6 +391,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.card__bottom-actions--comments {
|
||||
@extend .card__bottom-actions;
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.card__multi-pane {
|
||||
display: flex;
|
||||
|
||||
|
|
|
@ -165,7 +165,7 @@ $actions-z-index: 2;
|
|||
}
|
||||
|
||||
.channel__title {
|
||||
display: inline;
|
||||
display: flex;
|
||||
margin-right: var(--spacing-s);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -7,6 +7,14 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.comments--contracted {
|
||||
@extend .comments;
|
||||
max-height: 5rem;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: -webkit-gradient(linear, left 30%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.comments--replies {
|
||||
list-style-type: none;
|
||||
margin-left: var(--spacing-s);
|
||||
|
|
|
@ -60,3 +60,8 @@
|
|||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
|
||||
.embed__overlay-logo {
|
||||
max-height: 3.5rem;
|
||||
max-width: 12rem;
|
||||
}
|
||||
|
|
|
@ -178,6 +178,7 @@ const sharedStateFilters = {
|
|||
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
|
||||
sharing_3P: { source: 'app', property: 'allowAnalytics' },
|
||||
builtinCollections: { source: 'collections', property: 'builtin' },
|
||||
editedCollections: { source: 'collections', property: 'edited' },
|
||||
// savedCollections: { source: 'collections', property: 'saved' },
|
||||
unpublishedCollections: { source: 'collections', property: 'unpublished' },
|
||||
};
|
||||
|
|
|
@ -286,7 +286,7 @@ export function GetLinksData(
|
|||
? `>${Math.floor(moment().subtract(9, 'months').startOf('week').unix())}`
|
||||
: `>${Math.floor(moment().subtract(1, 'year').startOf('week').unix())}`,
|
||||
pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4),
|
||||
streamTypes: CS.FILE_TYPES,
|
||||
streamTypes: null,
|
||||
channelIds: subscribedChannels.map((subscription: Subscription) => {
|
||||
const { channelClaimId } = parseURI(subscription.uri);
|
||||
return channelClaimId;
|
||||
|
|
|
@ -38,7 +38,7 @@ function FileViewerEmbeddedEnded(props: Props) {
|
|||
<div className="file-viewer__overlay">
|
||||
<div className="file-viewer__overlay-secondary">
|
||||
<Button className="file-viewer__overlay-logo" href={URL}>
|
||||
<Logo type={'embed'} />
|
||||
<Logo type={'embed-ended'} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="file-viewer__overlay-title">{prompt}</div>
|
||||
|
|
|
@ -23,13 +23,17 @@ const { getJsBundleId } = require('../bundle-id.js');
|
|||
const jsBundleId = getJsBundleId();
|
||||
|
||||
function insertToHead(fullHtml, htmlToInsert) {
|
||||
return fullHtml.replace(
|
||||
/<!-- VARIABLE_HEAD_BEGIN -->.*<!-- VARIABLE_HEAD_END -->/s,
|
||||
`
|
||||
${htmlToInsert || buildOgMetadata()}
|
||||
<script src="/public/ui-${jsBundleId}.js" async></script>
|
||||
`
|
||||
);
|
||||
const beginStr = '<!-- VARIABLE_HEAD_BEGIN -->';
|
||||
const finalStr = '<!-- VARIABLE_HEAD_END -->';
|
||||
|
||||
const beginIndex = fullHtml.indexOf(beginStr);
|
||||
const finalIndex = fullHtml.indexOf(finalStr);
|
||||
|
||||
if (beginIndex > -1 && finalIndex > -1 && finalIndex > beginIndex) {
|
||||
return `${fullHtml.slice(0, beginIndex)}${
|
||||
htmlToInsert || buildOgMetadata()
|
||||
}<script src="/public/ui-${jsBundleId}.js" async></script>${fullHtml.slice(finalIndex + finalStr.length)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function truncateDescription(description) {
|
||||
|
|
|
@ -10136,9 +10136,9 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#a327385cdf71568dbd15a17f3dcf5f4b83e0966d:
|
||||
lbry-redux@lbryio/lbry-redux#7cc9923ed9ff1940b508842af6be44c8da906a60:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/a327385cdf71568dbd15a17f3dcf5f4b83e0966d"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/7cc9923ed9ff1940b508842af6be44c8da906a60"
|
||||
dependencies:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
|
Loading…
Reference in a new issue