Merge branch 'master' into protocol

This commit is contained in:
Baltazar Gomez 2021-08-03 16:43:28 -05:00 committed by GitHub
commit 429528608e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 376 additions and 99 deletions

View file

@ -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)) - 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)) - 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)) - 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 ### Changed
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500)) - Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))

View file

@ -152,7 +152,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "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", "lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -209,6 +209,7 @@
"Your comment": "Your comment", "Your comment": "Your comment",
"Post --[button to submit something]--": "Post", "Post --[button to submit something]--": "Post",
"Post --[noun, markdown post tab button]--": "Post", "Post --[noun, markdown post tab button]--": "Post",
"Livestream --[noun, livestream tab button]--": "Livestream --[noun, livestream tab button]--",
"Posting...": "Posting...", "Posting...": "Posting...",
"Incompatible daemon": "Incompatible daemon", "Incompatible daemon": "Incompatible daemon",
"Incompatible daemon running": "Incompatible daemon running", "Incompatible daemon running": "Incompatible daemon running",
@ -2066,5 +2067,10 @@
"Open in Desktop": "Open in Desktop", "Open in Desktop": "Open in Desktop",
"Show %count% replies": "Show %count% replies", "Show %count% replies": "Show %count% replies",
"Show reply": "Show reply", "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--" "--end--": "--end--"
} }

View file

@ -1,11 +1,17 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import CollectionAddButton from './view'; import CollectionAddButton from './view';
import { makeSelectClaimForUri } from 'lbry-redux'; import { makeSelectClaimForUri, makeSelectClaimUrlInCollection } from 'lbry-redux';
const select = (state, props) => ({ const select = (state, props) => {
claim: makeSelectClaimForUri(props.uri)(state), const claim = makeSelectClaimForUri(props.uri)(state);
}); const permanentUrl = claim && claim.permanent_url;
return {
claim,
isSaved: makeSelectClaimUrlInCollection(permanentUrl)(state),
};
};
export default connect(select, { export default connect(select, {
doOpenModal, doOpenModal,

View file

@ -11,10 +11,11 @@ type Props = {
fileAction?: boolean, fileAction?: boolean,
type?: boolean, type?: boolean,
claim: Claim, claim: Claim,
isSaved: boolean,
}; };
export default function CollectionAddButton(props: Props) { export default function CollectionAddButton(props: Props) {
const { doOpenModal, uri, fileAction, type = 'playlist', claim } = props; const { doOpenModal, uri, fileAction, type = 'playlist', claim, isSaved } = props;
// $FlowFixMe // $FlowFixMe
const streamType = (claim && claim.value && claim.value.stream_type) || ''; const streamType = (claim && claim.value && claim.value.stream_type) || '';
@ -25,9 +26,9 @@ export default function CollectionAddButton(props: Props) {
<Button <Button
button={fileAction ? undefined : 'alt'} button={fileAction ? undefined : 'alt'}
className={classnames({ 'button--file-action': fileAction })} 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} iconSize={fileAction ? 22 : undefined}
label={uri ? __('Save') : __('New List')} label={uri ? (!isSaved ? __('Save') : __('Saved')) : __('New List')}
requiresAuth={IS_WEB} requiresAuth={IS_WEB}
title={__('Add this claim to a list')} title={__('Add this claim to a list')}
onClick={(e) => { onClick={(e) => {

View file

@ -47,6 +47,7 @@ type Props = {
searchOptions?: any, searchOptions?: any,
collectionId?: string, collectionId?: string,
showNoSourceClaims?: boolean, showNoSourceClaims?: boolean,
onClick?: (e: any, index?: number) => void,
}; };
export default function ClaimList(props: Props) { export default function ClaimList(props: Props) {
@ -55,6 +56,7 @@ export default function ClaimList(props: Props) {
uris, uris,
headerAltControls, headerAltControls,
loading, loading,
id,
persistedStorageKey, persistedStorageKey,
empty, empty,
defaultSort, defaultSort,
@ -80,6 +82,7 @@ export default function ClaimList(props: Props) {
searchOptions, searchOptions,
collectionId, collectionId,
showNoSourceClaims, showNoSourceClaims,
onClick,
} = props; } = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); 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); setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
} }
function handleClaimClicked(e, index) {
if (onClick) {
onClick(e, index);
}
}
useEffect(() => { useEffect(() => {
const handleScroll = debounce((e) => { const handleScroll = debounce((e) => {
if (page && pageSize && onScrollBottom) { if (page && pageSize && onScrollBottom) {
@ -191,6 +200,8 @@ export default function ClaimList(props: Props) {
{injectedItem && index === 4 && <li>{injectedItem}</li>} {injectedItem && index === 4 && <li>{injectedItem}</li>}
<ClaimPreview <ClaimPreview
uri={uri} uri={uri}
containerId={id}
indexInContainer={index}
type={type} type={type}
active={activeUri && uri === activeUri} active={activeUri && uri === activeUri}
hideMenu={hideMenu} 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'; return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
}} }}
live={resolveLive(index)} live={resolveLive(index)}
onClick={handleClaimClicked}
/> />
</React.Fragment> </React.Fragment>
))} ))}

View file

@ -106,7 +106,7 @@ function ClaimListDiscover(props: Props) {
claimType, claimType,
pageSize, pageSize,
defaultClaimType, 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 defaultStreamType = SIMPLE_SITE ? CS.FILE_VIDEO : undefined, // add param for DEFAULT_STREAM_TYPE
freshness, freshness,
defaultFreshness = CS.FRESH_WEEK, defaultFreshness = CS.FRESH_WEEK,
@ -319,7 +319,7 @@ function ClaimListDiscover(props: Props) {
} }
if (streamTypeParam && streamTypeParam !== CS.CONTENT_ALL && claimType !== CS.CLAIM_CHANNEL) { if (streamTypeParam && streamTypeParam !== CS.CONTENT_ALL && claimType !== CS.CLAIM_CHANNEL) {
options.stream_types = [streamTypeParam]; options.stream_types = typeof streamTypeParam === 'string' ? [streamTypeParam] : streamTypeParam;
} }
if (claimTypeParam) { if (claimTypeParam) {

View file

@ -400,6 +400,13 @@ function ClaimMenuList(props: Props) {
)} )}
<hr className="menu__separator" /> <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 && ( {isChannelPage && IS_WEB && rssUrl && (
<MenuItem className="comment__menu-option" onSelect={handleCopyRssLink}> <MenuItem className="comment__menu-option" onSelect={handleCopyRssLink}>
<div className="menu__link"> <div className="menu__link">
@ -418,13 +425,6 @@ function ClaimMenuList(props: Props) {
</MenuItem> </MenuItem>
)} )}
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SHARE} />
{__('Copy Link')}
</div>
</MenuItem>
{!claimIsMine && !isMyCollection && ( {!claimIsMine && !isMyCollection && (
<MenuItem className="comment__menu-option" onSelect={handleReportContent}> <MenuItem className="comment__menu-option" onSelect={handleReportContent}>
<div className="menu__link"> <div className="menu__link">

View file

@ -29,6 +29,7 @@ import ClaimPreviewNoContent from './claim-preview-no-content';
import { ENABLE_NO_SOURCE_CLAIMS } from 'config'; import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import Button from 'component/button'; import Button from 'component/button';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import { CONTAINER_ID } from 'constants/navigation';
const AbandonedChannelPreview = lazyImport(() => const AbandonedChannelPreview = lazyImport(() =>
import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */) import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */)
@ -45,7 +46,7 @@ type Props = {
reflectingProgress?: any, // fxme reflectingProgress?: any, // fxme
resolveUri: (string) => void, resolveUri: (string) => void,
isResolvingUri: boolean, isResolvingUri: boolean,
history: { push: (string) => void }, history: { push: (string | any) => void },
title: string, title: string,
nsfw: boolean, nsfw: boolean,
placeholder: string, placeholder: string,
@ -65,7 +66,7 @@ type Props = {
actions: boolean | Node | string | number, actions: boolean | Node | string | number,
properties: boolean | Node | string | number | ((Claim) => Node), properties: boolean | Node | string | number | ((Claim) => Node),
empty?: Node, empty?: Node,
onClick?: (any) => any, onClick?: (e: any, index?: number) => any,
streamingUrl: ?string, streamingUrl: ?string,
getFile: (string) => void, getFile: (string) => void,
customShouldHide?: (Claim) => boolean, customShouldHide?: (Claim) => boolean,
@ -88,6 +89,8 @@ type Props = {
disableNavigation?: boolean, disableNavigation?: boolean,
mediaDuration?: string, mediaDuration?: string,
date?: any, 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) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -149,6 +152,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
isCollectionMine, isCollectionMine,
collectionUris, collectionUris,
disableNavigation, disableNavigation,
containerId,
indexInContainer,
} = props; } = props;
const isCollection = claim && claim.value_type === 'collection'; const isCollection = claim && claim.value_type === 'collection';
const collectionClaimId = isCollection && claim && claim.claim_id; 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); collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
navigateUrl = navigateUrl + `?` + collectionParams.toString(); navigateUrl = navigateUrl + `?` + collectionParams.toString();
} }
const handleNavLinkClick = (e) => {
if (onClick) {
onClick(e, indexInContainer);
}
e.stopPropagation();
};
const navLinkProps = { const navLinkProps = {
to: navigateUrl, to: {
onClick: (e) => e.stopPropagation(), 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 // 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) { function handleOnClick(e) {
if (onClick) { if (onClick) {
onClick(e); onClick(e, indexInContainer);
} }
if (claim && !pending && !disableNavigation) { if (claim && !pending && !disableNavigation) {
history.push(navigateUrl); history.push({
pathname: navigateUrl,
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
});
} }
} }

View file

@ -159,6 +159,12 @@ function ClaimTilesDiscover(props: Props) {
new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1])) new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1]))
); );
const liveUris = []; 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([]); const [prevUris, setPrevUris] = React.useState([]);
@ -192,8 +198,7 @@ function ClaimTilesDiscover(props: Props) {
channel_ids: channelIds || [], channel_ids: channelIds || [],
not_channel_ids: mutedAndBlockedChannelIds, not_channel_ids: mutedAndBlockedChannelIds,
order_by: orderBy || ['trending_group', 'trending_mixed'], order_by: orderBy || ['trending_group', 'trending_mixed'],
stream_types: stream_types: streamTypesParam,
streamTypes === null ? undefined : SIMPLE_SITE && !hasNoSource ? [CS.FILE_VIDEO, CS.FILE_AUDIO] : undefined,
}; };
if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) { if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) {

View file

@ -13,6 +13,7 @@ import usePersistedState from 'effects/use-persisted-state';
import { ENABLE_COMMENT_REACTIONS } from 'config'; import { ENABLE_COMMENT_REACTIONS } from 'config';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import { useIsMobile } from 'effects/use-screensize';
const DEBOUNCE_SCROLL_HANDLER_MS = 200; 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 DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT); const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const isMobile = useIsMobile();
const [expandedComments, setExpandedComments] = React.useState(!isMobile);
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
// Display comments immediately if not fetching reactions // Display comments immediately if not fetching reactions
@ -191,7 +194,7 @@ function CommentList(props: Props) {
} }
const handleCommentScroll = debounce(() => { const handleCommentScroll = debounce(() => {
if (shouldFetchNextPage(page, topLevelTotalPages, window, document)) { if (!isMobile && shouldFetchNextPage(page, topLevelTotalPages, window, document)) {
setPage(page + 1); setPage(page + 1);
} }
}, DEBOUNCE_SCROLL_HANDLER_MS); }, DEBOUNCE_SCROLL_HANDLER_MS);
@ -205,6 +208,7 @@ function CommentList(props: Props) {
} }
} }
}, [ }, [
isMobile,
page, page,
moreBelow, moreBelow,
spinnerRef, spinnerRef,
@ -279,7 +283,13 @@ function CommentList(props: Props) {
<Empty padded text={__('That was pretty deep. What do you think?')} /> <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 && {topLevelComments &&
displayedComments && displayedComments &&
displayedComments.map((comment) => { displayedComments.map((comment) => {
@ -307,7 +317,36 @@ function CommentList(props: Props) {
})} })}
</ul> </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}> <div className="main--empty" ref={spinnerRef}>
<Spinner type="small" /> <Spinner type="small" />
</div> </div>

View file

@ -31,7 +31,17 @@ export default function Logo(props: Props) {
if (LOGO_TEXT_LIGHT) { if (LOGO_TEXT_LIGHT) {
return ( 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 { } else {

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimIsNsfw, makeSelectClaimForUri } from 'lbry-redux'; import { makeSelectClaimIsNsfw, makeSelectClaimForUri } from 'lbry-redux';
import { doRecommendationUpdate, doRecommendationClicked } from 'redux/actions/content';
import { doFetchRecommendedContent } from 'redux/actions/search'; import { doFetchRecommendedContent } from 'redux/actions/search';
import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search'; import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -17,6 +18,9 @@ const select = (state, props) => ({
const perform = (dispatch) => ({ const perform = (dispatch) => ({
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)), 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); export default connect(select, perform)(RecommendedContent);

View file

@ -1,6 +1,8 @@
// @flow // @flow
import { SHOW_ADS } from 'config'; import { SHOW_ADS } from 'config';
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import Ads from 'web/component/ads'; import Ads from 'web/component/ads';
@ -8,6 +10,7 @@ import Card from 'component/common/card';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames'; import classnames from 'classnames';
import { CONTAINER_ID } from 'constants/navigation';
const VIEW_ALL_RELATED = 'view_all_related'; const VIEW_ALL_RELATED = 'view_all_related';
const VIEW_MORE_FROM = 'view_more_from'; const VIEW_MORE_FROM = 'view_more_from';
@ -21,6 +24,8 @@ type Props = {
mature: boolean, mature: boolean,
isAuthenticated: boolean, isAuthenticated: boolean,
claim: ?StreamClaim, 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) { export default React.memo<Props>(function RecommendedContent(props: Props) {
@ -33,33 +38,70 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
isSearching, isSearching,
isAuthenticated, isAuthenticated,
claim, claim,
doRecommendationUpdate,
doRecommendationClicked,
} = props; } = props;
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED); 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 signingChannel = claim && claim.signing_channel;
const channelName = signingChannel ? signingChannel.name : null; const channelName = signingChannel ? signingChannel.name : null;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMedium = useIsMediumScreen(); const isMedium = useIsMediumScreen();
function reorderList(recommendedContent) { React.useEffect(() => {
let newList = recommendedContent; function moveAutoplayNextItemToTop(recommendedContent) {
if (newList) { let newList = recommendedContent;
const index = newList.indexOf(nextRecommendedUri); if (newList) {
if (index === -1) { const index = newList.indexOf(nextRecommendedUri);
// This would be weird. Shouldn't happen since it is derived from the same list. if (index > 0) {
} else if (index !== 0) { const a = newList[0];
// Swap the "next" item to the top of the list newList[0] = nextRecommendedUri;
const a = newList[0]; newList[index] = a;
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(() => { React.useEffect(() => {
doFetchRecommendedContent(uri, mature); doFetchRecommendedContent(uri, mature);
}, [uri, mature, doFetchRecommendedContent]); }, [uri, mature, doFetchRecommendedContent]);
function handleRecommendationClicked(e: any, index: number) {
if (claim) {
doRecommendationClicked(claim.claim_id, index);
}
}
return ( return (
<Card <Card
isBodyList isBodyList
@ -91,12 +133,14 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
<div> <div>
{viewMode === VIEW_ALL_RELATED && ( {viewMode === VIEW_ALL_RELATED && (
<ClaimList <ClaimList
id={recommendationId}
type="small" type="small"
loading={isSearching} loading={isSearching}
uris={reorderList(recommendedContent)} uris={recommendationUrls}
hideMenu={isMobile} hideMenu={isMobile}
injectedItem={SHOW_ADS && IS_WEB && !isAuthenticated && <Ads small type={'video'} />} injectedItem={SHOW_ADS && IS_WEB && !isAuthenticated && <Ads small type={'video'} />}
empty={__('No related content found')} empty={__('No related content found')}
onClick={handleRecommendationClicked}
/> />
)} )}
{viewMode === VIEW_MORE_FROM && signingChannel && ( {viewMode === VIEW_MORE_FROM && signingChannel && (

View file

@ -7,7 +7,7 @@ import { makeSelectContentPositionForUri } from 'redux/selectors/content';
import VideoViewer from './view'; import VideoViewer from './view';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; 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 { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user'; import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
@ -31,6 +31,7 @@ const select = (state, props) => {
homepageData: selectHomepageData(state), homepageData: selectHomepageData(state),
authenticated: selectUserVerifiedEmail(state), authenticated: selectUserVerifiedEmail(state),
userId: userId, userId: userId,
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
}; };
}; };

View file

@ -1,6 +1,12 @@
// Created by xander on 6/21/2021 // Created by xander on 6/21/2021
import videojs from 'video.js'; 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 VERSION = '0.0.1';
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view'; const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
@ -17,23 +23,28 @@ const RecsysData = {
}; };
function createRecsys(claimId, userId, events, loadedAt, isEmbed) { function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
// TODO: use a UUID generator
const uuid = uuidV4();
const pageLoadedAt = loadedAt; const pageLoadedAt = loadedAt;
const pageExitedAt = Date.now(); const pageExitedAt = Date.now();
return {
uuid: uuid, if (window.store) {
parentUuid: null, const state = window.store.getState();
uid: userId,
claimId: claimId, return {
pageLoadedAt: pageLoadedAt, uuid: makeSelectRecommendationId(claimId)(state),
pageExitedAt: pageExitedAt, parentUuid: makeSelectRecommendationParentId(claimId)(state),
recsysId: recsysId, uid: userId,
recClaimIds: null, claimId: claimId,
recClickedVideoIdx: null, pageLoadedAt: pageLoadedAt,
events: events, pageExitedAt: pageExitedAt,
isEmbed: isEmbed, recsysId: recsysId,
}; recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
events: events,
isEmbed: isEmbed,
};
}
return undefined;
} }
function newRecsysEvent(eventType, offset, arg) { function newRecsysEvent(eventType, offset, arg) {
@ -130,7 +141,10 @@ class RecsysPlugin extends Component {
this.loadedAt, this.loadedAt,
false false
); );
sendRecsysEvents(event);
if (event) {
sendRecsysEvents(event);
}
} }
onPlay(event) { onPlay(event) {
@ -226,7 +240,7 @@ const onPlayerReady = (player, options) => {
* @function plugin * @function plugin
* @param {Object} [options={}] * @param {Object} [options={}]
*/ */
const plugin = function(options) { const plugin = function (options) {
this.ready(() => { this.ready(() => {
onPlayerReady(this, videojs.mergeOptions(defaults, options)); onPlayerReady(this, videojs.mergeOptions(defaults, options));
}); });

View file

@ -56,6 +56,7 @@ type Props = {
claimId: ?string, claimId: ?string,
userId: ?number, userId: ?number,
allowPreRoll: ?boolean, allowPreRoll: ?boolean,
shareTelemetry: boolean,
}; };
// type VideoJSOptions = { // type VideoJSOptions = {
@ -193,6 +194,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
claimId, claimId,
userId, userId,
// allowPreRoll, // allowPreRoll,
shareTelemetry,
} = props; } = props;
const [reload, setReload] = useState('initial'); const [reload, setReload] = useState('initial');
@ -564,11 +566,12 @@ export default React.memo<Props>(function VideoJs(props: Props) {
}); });
// Add recsys plugin // Add recsys plugin
// TODO: Add an if(odysee.com) around this function to only use recsys on odysee if (shareTelemetry) {
player.recsys({ player.recsys({
videoId: claimId, videoId: claimId,
userId: userId, userId: userId,
}); });
}
// set playsinline for mobile // set playsinline for mobile
// TODO: make this better // TODO: make this better

View file

@ -51,6 +51,7 @@ type Props = {
authenticated: boolean, authenticated: boolean,
userId: number, userId: number,
homepageData?: { [string]: HomepageCat }, homepageData?: { [string]: HomepageCat },
shareTelemetry: boolean,
}; };
/* /*
@ -84,6 +85,7 @@ function VideoViewer(props: Props) {
homepageData, homepageData,
authenticated, authenticated,
userId, userId,
shareTelemetry,
} = props; } = props;
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : []; const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
@ -319,6 +321,7 @@ function VideoViewer(props: Props) {
claimId={claimId} claimId={claimId}
userId={userId} userId={userId}
allowPreRoll={!embedded && !authenticated} allowPreRoll={!embedded && !authenticated}
shareTelemetry={shareTelemetry}
/> />
)} )}
</div> </div>

View file

@ -98,6 +98,8 @@ export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED'; 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_URI = 'CLEAR_CONTENT_HISTORY_URI';
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL'; export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
// Files // Files
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'; export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';

View file

@ -0,0 +1 @@
export const CONTAINER_ID = 'CONTAINER_ID';

View file

@ -125,9 +125,11 @@ function DiscoverPage(props: Props) {
// Assume wild west page if no dynamicRouteProps // Assume wild west page if no dynamicRouteProps
// Not a very good solution, but just doing it for now // Not a very good solution, but just doing it for now
// until we are sure this page will stay around // 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={ releaseTime={
SIMPLE_SITE SIMPLE_SITE
? !dynamicRouteProps && `>${Math.floor(moment().subtract(1, 'day').startOf('week').unix())}` ? !dynamicRouteProps && !tags && `>${Math.floor(moment().subtract(1, 'day').startOf('week').unix())}`
: undefined : undefined
} }
feeAmount={SIMPLE_SITE ? !dynamicRouteProps && CS.FEE_AMOUNT_ANY : undefined} feeAmount={SIMPLE_SITE ? !dynamicRouteProps && CS.FEE_AMOUNT_ANY : undefined}

View file

@ -40,7 +40,9 @@ function TopPage(props: Props) {
label={__('Repost Here')} 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% %repost% %publish%

View file

@ -43,7 +43,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
full_status: true, full_status: true,
page: 1, page: 1,
page_size: 1, page_size: 1,
}).then(result => { }).then((result) => {
const { items: fileInfos } = result; const { items: fileInfos } = result;
const fileInfo = fileInfos[0]; const fileInfo = fileInfos[0];
if (!fileInfo || fileInfo.written_bytes === 0) { if (!fileInfo || fileInfo.written_bytes === 0) {
@ -261,3 +261,21 @@ export function doClearContentHistoryAll() {
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL }); 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 },
});
}
};

View file

@ -7,6 +7,10 @@ const defaultState = {
channelClaimCounts: {}, channelClaimCounts: {},
positions: {}, positions: {},
history: [], history: [],
recommendationId: {}, // { "claimId": "recommendationId" }
recommendationParentId: {}, // { "claimId": "referrerId" }
recommendationUrls: {}, // { "claimId": [lbryUrls...] }
recommendationClicks: {}, // { "claimId": [clicked indices...] }
}; };
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) => 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 { uri, lastViewed } = action.data;
const { history } = state; const { history } = state;
const historyObj = { uri, lastViewed }; const historyObj = { uri, lastViewed };
const index = history.findIndex(i => i.uri === uri); const index = history.findIndex((i) => i.uri === uri);
const newHistory = const newHistory =
index === -1 index === -1
? [historyObj].concat(history) ? [historyObj].concat(history)
@ -84,7 +88,7 @@ reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => {
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => { reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
const { uri } = action.data; const { uri } = action.data;
const { history } = state; const { history } = state;
const index = history.findIndex(i => i.uri === uri); const index = history.findIndex((i) => i.uri === uri);
return index === -1 return index === -1
? state ? 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) => { // reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
// return { // return {

View file

@ -13,6 +13,7 @@ import {
makeSelectFileNameForUri, makeSelectFileNameForUri,
normalizeURI, normalizeURI,
selectMyActiveClaims, selectMyActiveClaims,
selectClaimIdsByUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search'; import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
@ -247,19 +248,37 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) =>
); );
export const makeSelectSigningIsMine = (rawUri: string) => { export const makeSelectSigningIsMine = (rawUri: string) => {
let uri; let uri;
try {
uri = normalizeURI(rawUri);
} catch (e) {}
return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
try { try {
uri = normalizeURI(rawUri); parseURI(uri);
} catch (e) { } } catch (e) {
return false;
}
const signingChannel = claims && claims[uri] && (claims[uri].signing_channel || claims[uri]);
return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => { return signingChannel && myClaims.has(signingChannel.claim_id);
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); 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]);

View file

@ -391,6 +391,11 @@
align-items: center; align-items: center;
} }
.card__bottom-actions--comments {
@extend .card__bottom-actions;
margin-top: var(--spacing-s);
}
.card__multi-pane { .card__multi-pane {
display: flex; display: flex;

View file

@ -165,7 +165,7 @@ $actions-z-index: 2;
} }
.channel__title { .channel__title {
display: inline; display: flex;
margin-right: var(--spacing-s); margin-right: var(--spacing-s);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View file

@ -7,6 +7,14 @@ $thumbnailWidthSmall: 1rem;
margin-top: var(--spacing-l); 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 { .comments--replies {
list-style-type: none; list-style-type: none;
margin-left: var(--spacing-s); margin-left: var(--spacing-s);

View file

@ -60,3 +60,8 @@
font-size: var(--font-large); font-size: var(--font-large);
} }
} }
.embed__overlay-logo {
max-height: 3.5rem;
max-width: 12rem;
}

View file

@ -178,6 +178,7 @@ const sharedStateFilters = {
app_welcome_version: { source: 'app', property: 'welcomeVersion' }, app_welcome_version: { source: 'app', property: 'welcomeVersion' },
sharing_3P: { source: 'app', property: 'allowAnalytics' }, sharing_3P: { source: 'app', property: 'allowAnalytics' },
builtinCollections: { source: 'collections', property: 'builtin' }, builtinCollections: { source: 'collections', property: 'builtin' },
editedCollections: { source: 'collections', property: 'edited' },
// savedCollections: { source: 'collections', property: 'saved' }, // savedCollections: { source: 'collections', property: 'saved' },
unpublishedCollections: { source: 'collections', property: 'unpublished' }, unpublishedCollections: { source: 'collections', property: 'unpublished' },
}; };

View file

@ -286,7 +286,7 @@ export function GetLinksData(
? `>${Math.floor(moment().subtract(9, 'months').startOf('week').unix())}` ? `>${Math.floor(moment().subtract(9, 'months').startOf('week').unix())}`
: `>${Math.floor(moment().subtract(1, 'year').startOf('week').unix())}`, : `>${Math.floor(moment().subtract(1, 'year').startOf('week').unix())}`,
pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4), pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4),
streamTypes: CS.FILE_TYPES, streamTypes: null,
channelIds: subscribedChannels.map((subscription: Subscription) => { channelIds: subscribedChannels.map((subscription: Subscription) => {
const { channelClaimId } = parseURI(subscription.uri); const { channelClaimId } = parseURI(subscription.uri);
return channelClaimId; return channelClaimId;

View file

@ -38,7 +38,7 @@ function FileViewerEmbeddedEnded(props: Props) {
<div className="file-viewer__overlay"> <div className="file-viewer__overlay">
<div className="file-viewer__overlay-secondary"> <div className="file-viewer__overlay-secondary">
<Button className="file-viewer__overlay-logo" href={URL}> <Button className="file-viewer__overlay-logo" href={URL}>
<Logo type={'embed'} /> <Logo type={'embed-ended'} />
</Button> </Button>
</div> </div>
<div className="file-viewer__overlay-title">{prompt}</div> <div className="file-viewer__overlay-title">{prompt}</div>

View file

@ -23,13 +23,17 @@ const { getJsBundleId } = require('../bundle-id.js');
const jsBundleId = getJsBundleId(); const jsBundleId = getJsBundleId();
function insertToHead(fullHtml, htmlToInsert) { function insertToHead(fullHtml, htmlToInsert) {
return fullHtml.replace( const beginStr = '<!-- VARIABLE_HEAD_BEGIN -->';
/<!-- VARIABLE_HEAD_BEGIN -->.*<!-- VARIABLE_HEAD_END -->/s, const finalStr = '<!-- VARIABLE_HEAD_END -->';
`
${htmlToInsert || buildOgMetadata()} const beginIndex = fullHtml.indexOf(beginStr);
<script src="/public/ui-${jsBundleId}.js" async></script> 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) { function truncateDescription(description) {

View file

@ -10136,9 +10136,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#a327385cdf71568dbd15a17f3dcf5f4b83e0966d: lbry-redux@lbryio/lbry-redux#7cc9923ed9ff1940b508842af6be44c8da906a60:
version "0.0.1" 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: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"