This commit is contained in:
Sean Yesmunt 2019-06-11 14:10:58 -04:00
parent be89a11904
commit 60543562aa
140 changed files with 2059 additions and 3305 deletions

View file

@ -60,7 +60,7 @@
"@exponent/electron-cookies": "^2.0.0", "@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "16.8", "@hot-loader/react-dom": "16.8",
"@lbry/color": "^1.0.2", "@lbry/color": "^1.0.2",
"@lbry/components": "^2.7.0", "@lbry/components": "^2.7.2",
"@reach/rect": "^0.2.1", "@reach/rect": "^0.2.1",
"@reach/tabs": "^0.1.5", "@reach/tabs": "^0.1.5",
"@types/three": "^0.93.1", "@types/three": "^0.93.1",
@ -119,7 +119,7 @@
"jsmediatags": "^3.8.1", "jsmediatags": "^3.8.1",
"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#02f6918238110726c0b3b4248c61a84ac0b969e3", "lbry-redux": "lbryio/lbry-redux#08ed1be3905896452536c92f17997bcde4533aea",
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845", "lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",
@ -153,6 +153,7 @@
"react-router": "^5.0.0", "react-router": "^5.0.0",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",
"react-simplemde-editor": "^4.0.0", "react-simplemde-editor": "^4.0.0",
"react-spring": "^8.0.20",
"react-toggle": "^4.0.2", "react-toggle": "^4.0.2",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-persist": "^4.8.0", "redux-persist": "^4.8.0",
@ -191,7 +192,7 @@
"yarn": "^1.3" "yarn": "^1.3"
}, },
"lbrySettings": { "lbrySettings": {
"lbrynetDaemonVersion": "0.37.4", "lbrynetDaemonVersion": "0.38.0rc6",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon", "lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet" "lbrynetDaemonFileName": "lbrynet"

View file

@ -84,19 +84,6 @@ export default appState => {
window.loadURL(rendererURL + deepLinkingURI); window.loadURL(rendererURL + deepLinkingURI);
setupBarMenu(); setupBarMenu();
// Windows back/forward mouse navigation
window.on('app-command', (e, cmd) => {
switch (cmd) {
case 'browser-backward':
window.webContents.send('navigate-backward', null);
break;
case 'browser-forward':
window.webContents.send('navigate-forward', null);
break;
default: // Do nothing
}
});
window.on('close', event => { window.on('close', event => {
if (!appState.isQuitting && !appState.autoUpdateAccepted) { if (!appState.isQuitting && !appState.autoUpdateAccepted) {
event.preventDefault(); event.preventDefault();

View file

@ -1,11 +1,7 @@
export const clipboard = () => { export const clipboard = () => {
throw 'Fix me!'; throw new Error('Fix me!');
}; };
export const ipcRenderer = () => { export const ipcRenderer = () => {
throw 'Fix me!'; throw new Error('Fix me!');
};
export const remote = () => {
throw 'Fix me!';
}; };

View file

@ -1,7 +1,6 @@
const callable = () => { const callable = () => {
throw Error('Need to fix this stub'); throw Error('Need to fix this stub');
}; };
const returningCallable = value => () => value;
export const remote = { export const remote = {
dialog: { dialog: {

View file

@ -1,22 +1,20 @@
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doUpdateBlockHeight, doError } from 'lbry-redux'; import { doUpdateBlockHeight, doError } from 'lbry-redux';
import { doToggleEnhancedLayout } from 'redux/actions/app'; import { selectUser, doRewardList, doFetchRewardedContent } from 'lbryinc';
import { selectUser } from 'lbryinc';
import { selectThemePath } from 'redux/selectors/settings'; import { selectThemePath } from 'redux/selectors/settings';
import { selectEnhancedLayout } from 'redux/selectors/app';
import App from './view'; import App from './view';
const select = state => ({ const select = state => ({
user: selectUser(state), user: selectUser(state),
theme: selectThemePath(state), theme: selectThemePath(state),
enhancedLayout: selectEnhancedLayout(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
alertError: errorList => dispatch(doError(errorList)), alertError: errorList => dispatch(doError(errorList)),
updateBlockHeight: () => dispatch(doUpdateBlockHeight()), updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
toggleEnhancedLayout: () => dispatch(doToggleEnhancedLayout()), fetchRewards: () => dispatch(doRewardList()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
}); });
export default hot( export default hot(

View file

@ -1,82 +1,54 @@
// @flow // @flow
import React from 'react'; import React, { useEffect, useRef } from 'react';
import Router from 'component/router/index'; import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter'; import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import SideBar from 'component/sideBar'; import SideBar from 'component/sideBar';
import Header from 'component/header'; import Header from 'component/header';
import { openContextMenu } from 'util/context-menu'; import { openContextMenu } from 'util/context-menu';
import EnhancedLayoutListener from 'util/enhanced-layout'; import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;
type Props = { type Props = {
alertError: (string | {}) => void, alertError: (string | {}) => void,
pageTitle: ?string, pageTitle: ?string,
language: string, language: string,
theme: string, theme: string,
updateBlockHeight: () => void, fetchRewards: () => void,
toggleEnhancedLayout: () => void, fetchRewardedContent: () => void,
enhancedLayout: boolean,
}; };
class App extends React.PureComponent<Props> { function App(props: Props) {
componentDidMount() { const { theme, fetchRewards, fetchRewardedContent } = props;
const { updateBlockHeight, toggleEnhancedLayout, alertError, theme } = this.props; const appRef = useRef();
const isEnhancedLayout = useKonamiListener();
// TODO: create type for this object useEffect(() => {
// it lives in jsonrpc.js ReactModal.setAppElement(appRef.current);
document.addEventListener('unhandledError', (event: any) => { fetchRewards();
alertError(event.detail); fetchRewardedContent();
}); }, [fetchRewards, fetchRewardedContent]);
useEffect(() => {
// $FlowFixMe // $FlowFixMe
document.documentElement.setAttribute('data-mode', theme); document.documentElement.setAttribute('data-mode', theme);
}, [theme]);
ReactModal.setAppElement('#window'); // fuck this
this.enhance = new EnhancedLayoutListener(() => toggleEnhancedLayout());
updateBlockHeight();
setInterval(() => {
updateBlockHeight();
}, TWO_POINT_FIVE_MINUTES);
}
componentDidUpdate(prevProps: Props) {
const { theme: prevTheme } = prevProps;
const { theme } = this.props;
if (prevTheme !== theme) {
// $FlowFixMe
document.documentElement.setAttribute('data-mode', theme);
}
}
componentWillUnmount() {
this.enhance = null;
}
enhance: ?any;
render() {
const { enhancedLayout } = this.props;
return ( return (
<div id="window" onContextMenu={e => openContextMenu(e)}> <div ref={appRef} onContextMenu={e => openContextMenu(e)}>
<Header /> <Header />
<SideBar />
<div className="main-wrapper"> <div className="main-wrapper">
<div className="main-wrapper-inner">
<Router /> <Router />
<SideBar />
</div>
</div> </div>
<ModalRouter /> <ModalRouter />
{enhancedLayout && <Yrbl className="yrbl--enhanced" />} {isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
</div> </div>
); );
} }
}
export default App; export default App;

View file

@ -62,8 +62,8 @@ class Button extends React.PureComponent<Props> {
'button--primary': button === 'primary', 'button--primary': button === 'primary',
'button--secondary': button === 'secondary', 'button--secondary': button === 'secondary',
'button--alt': button === 'alt', 'button--alt': button === 'alt',
'button--danger': button === 'danger',
'button--inverse': button === 'inverse', 'button--inverse': button === 'inverse',
'button--close': button === 'close',
'button--disabled': disabled, 'button--disabled': disabled,
'button--link': button === 'link', 'button--link': button === 'link',
'button--constrict': constrict, 'button--constrict': constrict,

View file

@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { doFetchClaimsByChannel } from 'redux/actions/content';
import { makeSelectCategoryListUris } from 'redux/selectors/content';
import { makeSelectFetchingChannelClaims, doResolveUris } from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import CategoryList from './view';
const select = (state, props) => ({
urisInList: makeSelectCategoryListUris(props.uris, props.categoryLink)(state),
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
obscureNsfw: !selectShowNsfw(state),
});
const perform = dispatch => ({
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
resolveUris: uris => dispatch(doResolveUris(uris, true)),
});
export default connect(
select,
perform
)(CategoryList);

View file

@ -1,316 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import React, { PureComponent, createRef } from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import ToolTip from 'component/common/tooltip';
import FileCard from 'component/fileCard';
import Button from 'component/button';
import SubscribeButton from 'component/subscribeButton';
import throttle from 'util/throttle';
import { formatLbryUriForWeb } from 'util/uri';
type Props = {
category: string,
categoryLink: ?string,
fetching: boolean,
obscureNsfw: boolean,
currentPageAttributes: { scrollY: number },
fetchChannel: string => void,
urisInList: ?Array<string>,
resolveUris: (Array<string>) => void,
lazyLoad: boolean, // only fetch rows if they are on the screen
};
type State = {
canScrollNext: boolean,
canScrollPrevious: boolean,
};
class CategoryList extends PureComponent<Props, State> {
static defaultProps = {
categoryLink: undefined,
lazyLoad: false,
};
scrollWrapper: { current: null | HTMLUListElement };
constructor() {
super();
this.state = {
canScrollPrevious: false,
canScrollNext: true,
};
(this: any).handleScrollNext = this.handleScrollNext.bind(this);
(this: any).handleScrollPrevious = this.handleScrollPrevious.bind(this);
(this: any).handleArrowButtonsOnScroll = this.handleArrowButtonsOnScroll.bind(this);
// (this: any).handleResolveOnScroll = this.handleResolveOnScroll.bind(this);
this.scrollWrapper = createRef();
}
componentDidMount() {
const { fetching, categoryLink, fetchChannel, resolveUris, urisInList, lazyLoad } = this.props;
if (!fetching && categoryLink && (!urisInList || !urisInList.length)) {
// Only fetch the channels claims if no urisInList are specifically passed in
// This allows setting a channel link and and passing in a custom list of urisInList (featured content usually works this way)
fetchChannel(categoryLink);
}
const scrollWrapper = this.scrollWrapper.current;
if (scrollWrapper) {
scrollWrapper.addEventListener('scroll', throttle(this.handleArrowButtonsOnScroll, 500));
if (!urisInList) {
return;
}
if (lazyLoad) {
if (window.innerHeight > scrollWrapper.offsetTop) {
resolveUris(urisInList);
}
} else {
resolveUris(urisInList);
}
}
}
// The old lazy loading for home page relied on the navigation reducers copy of the scroll height
// Keeping it commented out for now to try and find a better way for better TTI on the homepage
// componentDidUpdate(prevProps: Props) {
// const {scrollY: previousScrollY} = prevProps.currentPageAttributes;
// const {scrollY} = this.props.currentPageAttributes;
// if(scrollY > previousScrollY) {
// this.handleResolveOnScroll();
// }
// }
// handleResolveOnScroll() {
// const {
// urisInList,
// resolveUris,
// currentPageAttributes: {scrollY},
// } = this.props;
// const scrollWrapper = this.scrollWrapper.current;
// if(!scrollWrapper) {
// return;
// }
// const shouldResolve = window.innerHeight > scrollWrapper.offsetTop - scrollY;
// if(shouldResolve && urisInList) {
// resolveUris(urisInList);
// }
// }
handleArrowButtonsOnScroll() {
// Determine if the arrow buttons should be disabled
const scrollWrapper = this.scrollWrapper.current;
if (scrollWrapper) {
// firstElementChild and lastElementChild will always exist
// $FlowFixMe
const hasHiddenCardToLeft = !this.isCardVisible(scrollWrapper.firstElementChild);
// $FlowFixMe
const hasHiddenCardToRight = !this.isCardVisible(scrollWrapper.lastElementChild);
this.setState({
canScrollPrevious: hasHiddenCardToLeft,
canScrollNext: hasHiddenCardToRight,
});
}
}
handleScroll(scrollTarget: number) {
const scrollWrapper = this.scrollWrapper.current;
if (scrollWrapper) {
const currentScrollLeft = scrollWrapper.scrollLeft;
const direction = currentScrollLeft > scrollTarget ? 'left' : 'right';
this.scrollCardsAnimated(scrollWrapper, scrollTarget, direction);
}
}
scrollCardsAnimated = (scrollWrapper: HTMLUListElement, scrollTarget: number, direction: string) => {
let start;
const step = timestamp => {
if (!start) start = timestamp;
const currentLeftVal = scrollWrapper.scrollLeft;
let newTarget;
let shouldContinue;
let progress = currentLeftVal;
if (direction === 'right') {
progress += timestamp - start;
newTarget = Math.min(progress, scrollTarget);
shouldContinue = newTarget < scrollTarget;
} else {
progress -= timestamp - start;
newTarget = Math.max(progress, scrollTarget);
shouldContinue = newTarget > scrollTarget;
}
scrollWrapper.scrollLeft = newTarget;
if (shouldContinue) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
};
// check if a card is fully visible horizontally
isCardVisible = (card: HTMLLIElement): boolean => {
if (!card) {
return false;
}
const scrollWrapper = this.scrollWrapper.current;
if (scrollWrapper) {
const rect = card.getBoundingClientRect();
const isVisible = scrollWrapper.scrollLeft < card.offsetLeft && rect.left >= 0 && rect.right <= window.innerWidth;
return isVisible;
}
return false;
};
handleScrollNext() {
const scrollWrapper = this.scrollWrapper.current;
if (!scrollWrapper) {
return;
}
const cards = scrollWrapper.getElementsByTagName('li');
// Loop over items until we find one that is visible
// The card before that (starting from the end) is the new "first" card on the screen
let previousCard: ?HTMLLIElement;
for (let i = cards.length - 1; i > 0; i -= 1) {
const currentCard: HTMLLIElement = cards[i];
const currentCardVisible = this.isCardVisible(currentCard);
if (currentCardVisible && previousCard) {
const scrollTarget = previousCard.offsetLeft;
this.handleScroll(scrollTarget - cards[0].offsetLeft);
break;
}
previousCard = currentCard;
}
}
handleScrollPrevious() {
const scrollWrapper = this.scrollWrapper.current;
if (!scrollWrapper) {
return;
}
const cards = scrollWrapper.getElementsByTagName('li');
let hasFoundCard;
let numberOfCardsThatCanFit = 0;
// loop starting at the end until we find a visible card
// then count to find how many cards can fit on the screen
for (let i = cards.length - 1; i >= 0; i -= 1) {
const currentCard = cards[i];
const isCurrentCardVisible = this.isCardVisible(currentCard);
if (isCurrentCardVisible) {
if (!hasFoundCard) {
hasFoundCard = true;
}
numberOfCardsThatCanFit += 1;
} else if (hasFoundCard) {
// this card is off the screen to the left
// we know how many cards can fit on a screen
// find the new target and scroll
const firstCardOffsetLeft = cards[0].offsetLeft;
const cardIndexToScrollTo = i + 1 - numberOfCardsThatCanFit;
const newFirstCard = cards[cardIndexToScrollTo];
let scrollTarget;
if (newFirstCard) {
scrollTarget = newFirstCard.offsetLeft;
} else {
// more cards can fit on the screen than are currently hidden
// just scroll to the first card
scrollTarget = cards[0].offsetLeft;
}
scrollTarget -= firstCardOffsetLeft; // to play nice with the margins
this.handleScroll(scrollTarget);
break;
}
}
}
render() {
const { category, categoryLink, urisInList, obscureNsfw, lazyLoad } = this.props;
const { canScrollNext, canScrollPrevious } = this.state;
const isCommunityTopBids = category.match(/^community/i);
const showScrollButtons = isCommunityTopBids ? !obscureNsfw : true;
let channelLink;
if (categoryLink) {
channelLink = formatLbryUriForWeb(categoryLink);
}
return (
<section className="media-group--row">
<header className="media-group__header">
<h2 className="media-group__header-title">
{categoryLink ? (
<React.Fragment>
<Button label={category} navigate={channelLink} />
<SubscribeButton button="alt" showSnackBarOnSubscribe uri={`lbry://${categoryLink}`} />
</React.Fragment>
) : (
<span>{category}</span>
)}
{isCommunityTopBids && (
<ToolTip
direction="top"
label={__("What's this?")}
body={__(
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names from "one" to "ten" to put your content here!'
)}
/>
)}
</h2>
{showScrollButtons && (
<nav className="media-group__header-navigation">
<Button disabled={!canScrollPrevious} onClick={this.handleScrollPrevious} icon={ICONS.ARROW_LEFT} />
<Button disabled={!canScrollNext} onClick={this.handleScrollNext} icon={ICONS.ARROW_RIGHT} />
</nav>
)}
</header>
{obscureNsfw && isCommunityTopBids ? (
<p className="media__message help--warning">
{__(
'The community top bids section is only visible if you allow mature content in the app. You can change your content viewing preferences'
)}{' '}
<Button button="link" navigate="/$/settings" label={__('here')} />.
</p>
) : (
<ul className="media-scrollhouse" ref={this.scrollWrapper}>
{urisInList &&
urisInList.map(uri => (
<FileCard placeholder preventResolve={lazyLoad} showSubscribedLogo key={uri} uri={normalizeURI(uri)} />
))}
{!urisInList && new Array(10).fill(1).map((x, i) => <FileCard placeholder key={i} />)}
</ul>
)}
</section>
);
}
}
export default CategoryList;

View file

@ -22,8 +22,8 @@ function ChannelContent(props: Props) {
const showAbout = description || email || website; const showAbout = description || email || website;
return ( return (
<section> <section className="card--section">
{!showAbout && <h2 className="empty">{__('Nothing here yet')}</h2>} {!showAbout && <h2 className="main--empty empty">{__('Nothing here yet')}</h2>}
{showAbout && ( {showAbout && (
<Fragment> <Fragment>
{description && ( {description && (

View file

@ -19,7 +19,6 @@ type Props = {
function ChannelContent(props: Props) { function ChannelContent(props: Props) {
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props; const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props;
const hasContent = Boolean(claimsInChannel && claimsInChannel.length); const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
return ( return (
<Fragment> <Fragment>
{fetching && !hasContent && ( {fetching && !hasContent && (
@ -28,11 +27,15 @@ function ChannelContent(props: Props) {
</section> </section>
)} )}
{!fetching && !hasContent && <h2 className="empty">{__("This channel hasn't uploaded anything.")}</h2>} {!fetching && !hasContent && (
<div className="card--section">
<h2 className="card__content help">{__("This channel hasn't uploaded anything.")}</h2>
</div>
)}
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />} {!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
{hasContent && <FileList sortByHeight hideFilter fileInfos={claimsInChannel} />} {hasContent && <FileList noHeader uris={claimsInChannel.map(claim => claim.permanent_url)} />}
<Paginate <Paginate
onPageChange={page => fetchClaims(uri, page)} onPageChange={page => fetchClaims(uri, page)}

View file

@ -7,24 +7,25 @@ import Gerbil from './gerbil.png';
type Props = { type Props = {
thumbnail: ?string, thumbnail: ?string,
uri: string, uri: string,
className?: string,
}; };
function ChannelThumbnail(props: Props) { function ChannelThumbnail(props: Props) {
const { thumbnail, uri } = props; const { thumbnail, uri, className } = props;
// Generate a random color class based on the first letter of the channel name // Generate a random color class based on the first letter of the channel name
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
const initializer = channelName.charCodeAt(0) - 65; // will be between 0 and 57 const initializer = channelName.charCodeAt(0) - 65; // will be between 0 and 57
const className = `channel-thumbnail__default--${initializer % 4}`; const colorClassName = `channel-thumbnail__default--${initializer % 4}`;
return ( return (
<div <div
className={classnames('channel-thumbnail', { className={classnames('channel-thumbnail', className, {
[className]: !thumbnail, [colorClassName]: !thumbnail,
})} })}
> >
{!thumbnail && <img className="channel-thumbnail__default" src={Gerbil} />} {!thumbnail && <img className="channel-thumbnail__default" src={Gerbil} />}
{thumbnail && <img className="channel-thumbnail__custom" src={thumbnail} />} {thumbnail && <img className={classnames('channel-thumbnail__custom', className)} src={thumbnail} />}
</div> </div>
); );
} }

View file

@ -1,23 +0,0 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectIsUriResolving,
makeSelectTotalItemsForChannel,
} from 'lbry-redux';
import ChannelTile from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
totalItems: makeSelectTotalItemsForChannel(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform
)(ChannelTile);

View file

@ -1,89 +0,0 @@
// @flow
import * as React from 'react';
import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text';
import classnames from 'classnames';
import SubscribeButton from 'component/subscribeButton';
import { withRouter } from 'react-router-dom';
import { formatLbryUriForWeb } from 'util/uri';
type Props = {
uri: string,
isResolvingUri: boolean,
totalItems: number,
size: string,
claim: ?ChannelClaim,
resolveUri: string => void,
history: { push: string => void },
};
class ChannelTile extends React.PureComponent<Props> {
static defaultProps = {
size: 'regular',
};
componentDidMount() {
const { uri, resolveUri } = this.props;
resolveUri(uri);
}
componentWillReceiveProps(nextProps: Props) {
const { uri, resolveUri } = this.props;
if (nextProps.uri !== uri) {
resolveUri(uri);
}
}
render() {
const { claim, isResolvingUri, totalItems, uri, size, history } = this.props;
let channelName;
let subscriptionUri;
if (claim) {
channelName = claim.name;
subscriptionUri = claim.permanent_url;
}
const onClick = () => history.push(formatLbryUriForWeb(uri));
return (
<section
onClick={onClick}
role="button"
className={classnames('media-tile card--link', {
'media-tile--small': size === 'small',
'media-tile--large': size === 'large',
})}
>
<CardMedia title={channelName} thumbnail={null} />
<div className="media__info">
{isResolvingUri && <div className="media__title">{__('Loading...')}</div>}
{!isResolvingUri && (
<React.Fragment>
<div className="media__title">
<TruncatedText text={channelName || uri} lines={1} />
</div>
<div className="media__subtitle">
{totalItems > 0 && (
<span>
{totalItems} {totalItems === 1 ? 'publish' : 'publishes'}
</span>
)}
{!isResolvingUri && !totalItems && <span>This is an empty channel.</span>}
</div>
</React.Fragment>
)}
{subscriptionUri && (
<div className="media__actions">
<SubscribeButton buttonStyle="inverse" uri={subscriptionUri} />
</div>
)}
</div>
</section>
);
}
}
export default withRouter(ChannelTile);

View file

@ -10,7 +10,6 @@ type Props = {
showFullPrice: boolean, showFullPrice: boolean,
showPlus: boolean, showPlus: boolean,
isEstimate?: boolean, isEstimate?: boolean,
large?: boolean,
showLBC?: boolean, showLBC?: boolean,
fee?: boolean, fee?: boolean,
badge?: boolean, badge?: boolean,
@ -27,7 +26,7 @@ class CreditAmount extends React.PureComponent<Props> {
}; };
render() { render() {
const { amount, precision, showFullPrice, showFree, showPlus, large, isEstimate, fee, showLBC, badge } = this.props; const { amount, precision, showFullPrice, showFree, showPlus, isEstimate, fee, showLBC, badge } = this.props;
const minimumRenderableAmount = 10 ** (-1 * precision); const minimumRenderableAmount = 10 ** (-1 * precision);
const fullPrice = formatFullPrice(amount, 2); const fullPrice = formatFullPrice(amount, 2);
@ -69,7 +68,6 @@ class CreditAmount extends React.PureComponent<Props> {
badge, badge,
'badge--cost': badge && amount > 0, 'badge--cost': badge && amount > 0,
'badge--free': badge && isFree, 'badge--free': badge && isFree,
'badge--large': large,
})} })}
> >
{amountText} {amountText}

View file

@ -12,10 +12,6 @@ const select = state => ({
}); });
const perform = dispatch => () => ({ const perform = dispatch => () => ({
completeFirstRun: () => {
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
},
acknowledgeEmail: () => { acknowledgeEmail: () => {
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true)); dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
}, },

View file

@ -1,154 +0,0 @@
// @flow
import * as icons from 'constants/icons';
import * as React from 'react';
import { normalizeURI, convertToShareLink } from 'lbry-redux';
import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text';
import Icon from 'component/common/icon';
import UriIndicator from 'component/uriIndicator';
import classnames from 'classnames';
import FilePrice from 'component/filePrice';
import { openCopyLinkMenu } from 'util/context-menu';
import DateTime from 'component/dateTime';
import { withRouter } from 'react-router-dom';
import { formatLbryUriForWeb } from 'util/uri';
type Props = {
uri: string,
claim: ?StreamClaim,
fileInfo: ?{},
metadata: ?StreamMetadata,
rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean,
claimIsMine: boolean,
pending?: boolean,
resolveUri: string => void,
isResolvingUri: boolean,
isSubscribed: boolean,
isNew: boolean,
placeholder: boolean,
preventResolve: boolean,
history: { push: string => void },
thumbnail: string,
title: string,
nsfw: boolean,
};
class FileCard extends React.PureComponent<Props> {
static defaultProps = {
placeholder: false,
preventResolve: false,
};
componentDidMount() {
if (!this.props.preventResolve) {
this.resolve(this.props);
}
}
componentDidUpdate() {
if (!this.props.preventResolve) {
this.resolve(this.props);
}
}
resolve = (props: Props) => {
const { isResolvingUri, resolveUri, claim, uri, pending } = props;
if (!pending && !isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
}
};
render() {
const {
claim,
fileInfo,
rewardedContentClaimIds,
obscureNsfw,
claimIsMine,
pending,
isSubscribed,
isNew,
isResolvingUri,
placeholder,
history,
thumbnail,
title,
nsfw,
} = this.props;
const abandoned = !isResolvingUri && !claim && !pending && !placeholder;
if (abandoned) {
return null;
}
if (!claim && (!pending || placeholder)) {
return (
<li className="media-card media-placeholder">
<div className="media__thumb placeholder" />
<div className="media__title placeholder" />
<div className="media__channel placeholder" />
<div className="media__date placeholder" />
<div className="media__properties" />
</li>
);
}
// fix to use tags - one of many nsfw tags...
const shouldHide = !claimIsMine && !pending && obscureNsfw && nsfw;
if (shouldHide) {
return null;
}
const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const handleContextMenu = event => {
event.preventDefault();
event.stopPropagation();
if (claim) {
openCopyLinkMenu(convertToShareLink(claim.permanent_url), event);
}
};
const onClick = e => {
e.stopPropagation();
history.push(formatLbryUriForWeb(uri));
};
return (
<li
tabIndex="0"
role="button"
onClick={!pending && claim ? onClick : () => {}}
className={classnames('media-card', {
'card--link': !pending,
'media--pending': pending,
})}
onContextMenu={handleContextMenu}
>
<CardMedia thumbnail={thumbnail} />
<div className="media__title">
<TruncatedText text={title} lines={2} />
</div>
<div className="media__subtitle">
{pending ? <div>Pending...</div> : <UriIndicator uri={uri} link />}
<div>
<DateTime timeAgo uri={uri} />
</div>
</div>
<div className="media__properties">
<FilePrice hideFree uri={uri} />
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{isSubscribed && <Icon icon={icons.SUBSCRIPTION} />}
{claimIsMine && <Icon icon={icons.PUBLISHED} />}
{!claimIsMine && fileInfo && <Icon icon={icons.DOWNLOAD} />}
{isNew && <span className="badge badge--alert">{__('NEW')}</span>}
</div>
</li>
);
}
}
export default withRouter(FileCard);

View file

@ -1,14 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectClaimsById, doSetFileListSort } from 'lbry-redux';
import FileList from './view'; import FileList from './view';
const select = state => ({ const select = state => ({});
claimsById: selectClaimsById(state),
});
const perform = dispatch => ({ const perform = dispatch => ({});
setFileListSort: (page, value) => dispatch(doSetFileListSort(page, value)),
});
export default connect( export default connect(
select, select,

View file

@ -1,165 +1,70 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { buildURI, SORT_OPTIONS } from 'lbry-redux'; import classnames from 'classnames';
import { FormField, Form } from 'component/common/form'; import FileListItem from 'component/fileListItem';
import FileCard from 'component/fileCard'; import Spinner from 'component/spinner';
import { FormField } from 'component/common/form';
import usePersistedState from 'util/use-persisted-state';
const SORT_NEW = 'new';
const SORT_OLD = 'old';
type Props = { type Props = {
hideFilter: boolean, uris: Array<string>,
sortByHeight?: boolean, header: React.Node,
claimsById: Array<StreamClaim>, headerAltControls: React.Node,
fileInfos: Array<FileListItem>, injectedItem?: React.Node,
sortBy: string, loading: boolean,
page?: string, noHeader?: boolean,
setFileListSort: (?string, string) => void, slim?: string,
empty?: string,
// If using the default header, this is a unique ID needed to persist the state of the filter setting
persistedStorageKey?: string,
}; };
class FileList extends React.PureComponent<Props> { export default function FileList(props: Props) {
static defaultProps = { const { uris, header, headerAltControls, injectedItem, loading, persistedStorageKey, noHeader, slim, empty } = props;
hideFilter: false, const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey || 'file-list-global-sort', SORT_NEW);
sortBy: SORT_OPTIONS.DATE_NEW, const sortedUris = uris && currentSort === SORT_OLD ? uris.reverse() : uris;
}; const hasUris = uris && !!uris.length;
constructor(props: Props) { function handleSortChange() {
super(props); setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
this.sortFunctions = {
[SORT_OPTIONS.DATE_NEW]: fileInfos =>
this.props.sortByHeight
? fileInfos.sort((fileInfo1, fileInfo2) => {
if (fileInfo1.confirmations < 1) {
return -1;
} else if (fileInfo2.confirmations < 1) {
return 1;
} }
const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height
: 0;
const height2 = this.props.claimsById[fileInfo2.claim_id]
? this.props.claimsById[fileInfo2.claim_id].height
: 0;
if (height1 !== height2) {
// flipped because heigher block height is newer
return height2 - height1;
}
if (fileInfo1.absolute_channel_position && fileInfo2.absolute_channel_position) {
return fileInfo1.absolute_channel_position - fileInfo2.absolute_channel_position;
}
return 0;
})
: [...fileInfos].reverse(),
[SORT_OPTIONS.DATE_OLD]: fileInfos =>
this.props.sortByHeight
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height
: 999999;
const height2 = this.props.claimsById[fileInfo2.claim_id]
? this.props.claimsById[fileInfo2.claim_id].height
: 999999;
if (height1 < height2) {
return -1;
} else if (height1 > height2) {
return 1;
}
return 0;
})
: fileInfos,
[SORT_OPTIONS.TITLE]: fileInfos =>
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const getFileTitle = fileInfo => {
const { value, name, claim_name: claimName } = fileInfo;
if (value) {
return value.title || claimName;
}
// Invalid claim
return '';
};
const title1 = getFileTitle(fileInfo1).toLowerCase();
const title2 = getFileTitle(fileInfo2).toLowerCase();
if (title1 < title2) {
return -1;
} else if (title1 > title2) {
return 1;
}
return 0;
}),
[SORT_OPTIONS.FILENAME]: fileInfos =>
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
const fileName1Lower = fileName1.toLowerCase();
const fileName2Lower = fileName2.toLowerCase();
if (fileName1Lower < fileName2Lower) {
return -1;
} else if (fileName2Lower > fileName1Lower) {
return 1;
}
return 0;
}),
};
}
getChannelSignature = (fileInfo: { pending: boolean } & FileListItem) => {
if (fileInfo.pending) {
return undefined;
}
return fileInfo.channel_claim_id;
};
handleSortChanged(event: SyntheticInputEvent<*>) {
this.props.setFileListSort(this.props.page, event.target.value);
}
sortFunctions: {};
render() {
const { fileInfos, hideFilter, sortBy } = this.props;
const content = [];
if (!fileInfos) {
return null;
}
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId, txid, nout, isNew } = fileInfo;
const uriParams = {};
// This is unfortunate
// https://github.com/lbryio/lbry/issues/1159
const name = claimName || claimNameDownloaded;
uriParams.contentName = name;
uriParams.claimId = claimId;
const uri = buildURI(uriParams);
const outpoint = `${txid}:${nout}`;
// See https://github.com/lbryio/lbry-desktop/issues/1327 for discussion around using outpoint as the key
content.push(<FileCard key={outpoint} uri={uri} isNew={isNew} />);
});
return ( return (
<section> <section className={classnames('file-list')}>
{!hideFilter && ( {!noHeader && (
<Form> <div className="file-list__header">
<FormField label={__('Sort by')} type="select" value={sortBy} onChange={this.handleSortChanged}> {header || (
<option value={SORT_OPTIONS.DATE_NEW}>{__('Newest First')}</option> <FormField
<option value={SORT_OPTIONS.DATE_OLD}>{__('Oldest First')}</option> className="file-list__dropdown"
<option value={SORT_OPTIONS.TITLE}>{__('Title')}</option> type="select"
name="file_sort"
value={currentSort}
onChange={handleSortChange}
>
<option value={SORT_NEW}>{__('Newest First')}</option>
<option value={SORT_OLD}>{__('Oldest First')}</option>
</FormField> </FormField>
</Form>
)} )}
{loading && <Spinner light type="small" />}
<section className="media-group--list"> <div className="file-list__alt-controls">{headerAltControls}</div>
<div className="card__list">{content}</div> </div>
</section> )}
{hasUris && (
<ul>
{sortedUris.map((uri, index) => (
<React.Fragment key={uri}>
<FileListItem uri={uri} slim={slim} />
{index === 4 && injectedItem && <li className="file-list__item--injected">{injectedItem}</li>}
</React.Fragment>
))}
</ul>
)}
{!hasUris && !loading && (
<div className="main--empty">{empty || <h3 className="card__title">{__('No results')}</h3>}</div>
)}
</section> </section>
); );
} }
}
export default FileList;

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { selectFollowedTags, doClaimSearch, selectLastClaimSearchUris, selectFetchingClaimSearch } from 'lbry-redux';
import FileListDiscover from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
uris: selectLastClaimSearchUris(state),
loading: selectFetchingClaimSearch(state),
});
const perform = {
doClaimSearch,
};
export default connect(
select,
perform
)(FileListDiscover);

View file

@ -0,0 +1,139 @@
// @flow
import React, { useEffect } from 'react';
import { FormField } from 'component/common/form';
import FileList from 'component/fileList';
import moment from 'moment';
import usePersistedState from 'util/use-persisted-state';
const TIME_DAY = 'day';
const TIME_WEEK = 'week';
const TIME_MONTH = 'month';
const TIME_YEAR = 'year';
const TIME_ALL = 'all';
const TRENDING_SORT_YOU = 'you';
const TRENDING_SORT_ALL = 'everyone';
const TYPE_TRENDING = 'trending';
const TYPE_TOP = 'top';
const TYPE_NEW = 'new';
const TRENDING_FILTER_TYPES = [TRENDING_SORT_YOU, TRENDING_SORT_ALL];
const TRENDING_TYPES = ['trending', 'top', 'new'];
const TRENDING_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
type Props = {
uris: Array<string>,
doClaimSearch: (number, {}) => void,
injectedItem: any,
tags: Array<string>,
loading: boolean,
personal: boolean,
};
function FileListDiscover(props: Props) {
const { doClaimSearch, uris, tags, loading, personal, injectedItem } = props;
const [personalSort, setPersonalSort] = usePersistedState('file-list-trending:personalSort', TRENDING_SORT_YOU);
const [typeSort, setTypeSort] = usePersistedState('file-list-trending:typeSort', TYPE_TRENDING);
const [timeSort, setTimeSort] = usePersistedState('file-list-trending:timeSort', TIME_WEEK);
const toCapitalCase = string => string.charAt(0).toUpperCase() + string.slice(1);
const tagsString = tags.join(',');
useEffect(() => {
const options = {};
const newTags = tagsString.split(',');
if (personalSort === TRENDING_SORT_YOU) {
options.any_tags = newTags;
}
if (typeSort === TYPE_TRENDING) {
options.order_by = ['trending_global', 'trending_mixed'];
} else if (typeSort === TYPE_NEW) {
options.order_by = ['release_time'];
} else if (typeSort === TYPE_TOP) {
options.order_by = ['effective_amount'];
if (timeSort !== TIME_ALL) {
const time = Math.floor(
moment()
.subtract(1, timeSort)
.unix()
);
options.release_time = `>${time}`;
}
}
doClaimSearch(20, options);
}, [personalSort, typeSort, timeSort, doClaimSearch, tagsString]);
const header = (
<React.Fragment>
<h1 className={`card__title--flex`}>
{toCapitalCase(typeSort)} {'For'}
</h1>
{!personal && tags && tags.length ? (
tags.map(tag => (
<span key={tag} className="tag tag--remove" disabled>
{tag}
</span>
))
) : (
<FormField
type="select"
name="trending_overview"
className="file-list__dropdown"
value={personalSort}
onChange={e => setPersonalSort(e.target.value)}
>
{TRENDING_FILTER_TYPES.map(type => (
<option key={type} value={type}>
{toCapitalCase(type)}
</option>
))}
</FormField>
)}
</React.Fragment>
);
const headerAltControls = (
<React.Fragment>
<FormField
className="file-list__dropdown"
type="select"
name="trending_sort"
value={typeSort}
onChange={e => setTypeSort(e.target.value)}
>
{TRENDING_TYPES.map(type => (
<option key={type} value={type}>
{toCapitalCase(type)}
</option>
))}
</FormField>
{typeSort === 'top' && (
<FormField
className="file-list__dropdown"
type="select"
name="trending_time"
value={timeSort}
onChange={e => setTimeSort(e.target.value)}
>
{TRENDING_TIMES.map(time => (
<option key={time} value={time}>
{toCapitalCase(time)}
</option>
))}
</FormField>
)}
</React.Fragment>
);
return (
<FileList
loading={loading}
uris={uris}
injectedItem={personalSort === TRENDING_SORT_YOU && injectedItem}
header={header}
headerAltControls={headerAltControls}
/>
);
}
export default FileListDiscover;

View file

@ -2,8 +2,6 @@ import { connect } from 'react-redux';
import { import {
doResolveUri, doResolveUri,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectIsUriResolving, makeSelectIsUriResolving,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectClaimIsPending, makeSelectClaimIsPending,
@ -11,25 +9,15 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc';
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions'; import FileListItem from './view';
import { doClearContentHistoryUri } from 'redux/actions/content';
import FileCard from './view';
const select = (state, props) => ({ const select = (state, props) => ({
pending: makeSelectClaimIsPending(props.uri)(state), pending: makeSelectClaimIsPending(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
position: makeSelectContentPositionForUri(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state), thumbnail: makeSelectThumbnailForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state), nsfw: makeSelectClaimIsNsfw(props.uri)(state),
@ -37,10 +25,9 @@ const select = (state, props) => ({
const perform = dispatch => ({ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)),
}); });
export default connect( export default connect(
select, select,
perform perform
)(FileCard); )(FileListItem);

View file

@ -0,0 +1,132 @@
// @flow
import React, { useEffect } from 'react';
import classnames from 'classnames';
import { convertToShareLink } from 'lbry-redux';
import { withRouter } from 'react-router-dom';
import { openCopyLinkMenu } from 'util/context-menu';
import { formatLbryUriForWeb } from 'util/uri';
import CardMedia from 'component/cardMedia';
import UriIndicator from 'component/uriIndicator';
import TruncatedText from 'component/common/truncated-text';
import DateTime from 'component/dateTime';
import FileProperties from 'component/fileProperties';
import FileTags from 'component/fileTags';
import SubscribeButton from 'component/subscribeButton';
import ChannelThumbnail from 'component/channelThumbnail';
type Props = {
uri: string,
claim: ?Claim,
obscureNsfw: boolean,
claimIsMine: boolean,
pending?: boolean,
resolveUri: string => void,
isResolvingUri: boolean,
preventResolve: boolean,
history: { push: string => void },
thumbnail: string,
title: string,
nsfw: boolean,
large: boolean,
placeholder: boolean,
slim: boolean,
};
function FileListItem(props: Props) {
const {
obscureNsfw,
claimIsMine,
pending,
history,
uri,
isResolvingUri,
thumbnail,
title,
nsfw,
resolveUri,
claim,
large,
placeholder,
slim,
} = props;
const haventFetched = claim === undefined;
const abandoned = !isResolvingUri && !claim;
const shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw);
const isChannel = claim && claim.value_type === 'channel';
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
function handleContextMenu(e) {
e.preventDefault();
e.stopPropagation();
if (claim) {
openCopyLinkMenu(convertToShareLink(claim.permanent_url), e);
}
}
function onClick(e) {
if ((isChannel || title) && !pending) {
history.push(formatLbryUriForWeb(uri));
}
}
useEffect(() => {
if (!isResolvingUri && haventFetched && uri) {
resolveUri(uri);
}
}, [isResolvingUri, uri, resolveUri, haventFetched]);
if (shouldHide) {
return null;
}
if (placeholder && !claim) {
return (
<li className="file-list__item" disabled>
<div className="placeholder media__thumb" />
<div className="placeholder__wrapper">
<div className="placeholder file-list__item-title" />
<div className="placeholder media__subtitle" />
</div>
</li>
);
}
return (
<li
role="link"
onClick={onClick}
onContextMenu={handleContextMenu}
className={classnames('file-list__item', {
'file-list__item--large': large,
})}
>
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}
<div className="file-list__item-metadata">
<div className="file-list__item-info">
<div className="file-list__item-title">
<TruncatedText text={title || (claim && claim.name)} lines={1} />
</div>
{!slim && (
<div>
{isChannel && <SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />}
<FileProperties uri={uri} />
</div>
)}
</div>
<div className="file-list__item-properties">
<div className="media__subtitle">
<UriIndicator uri={uri} link />
{pending && <div>Pending...</div>}
<div>{isChannel ? `${claimsInChannel} ${__('publishes')}` : <DateTime timeAgo uri={uri} />}</div>
</div>
{!slim && <FileTags uri={uri} />}
</div>
</div>
</li>
);
}
export default withRouter(FileListItem);

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import {
makeSelectSearchUris,
selectIsSearching,
selectSearchDownloadUris,
makeSelectQueryWithOptions,
} from 'lbry-redux';
import FileListSearch from './view';
const select = (state, props) => ({
uris: makeSelectSearchUris(makeSelectQueryWithOptions()(state))(state),
downloadUris: selectSearchDownloadUris(props.query)(state),
isSearching: selectIsSearching(state),
});
export default connect(select)(FileListSearch);

View file

@ -1,44 +0,0 @@
// @flow
import * as React from 'react';
import { parseURI } from 'lbry-redux';
import FileTile from 'component/fileTile';
import ChannelTile from 'component/channelTile';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
const NoResults = () => <div className="file-tile">{__('No results')}</div>;
type Props = {
query: string,
isSearching: boolean,
uris: ?Array<string>,
};
class FileListSearch extends React.PureComponent<Props> {
render() {
const { uris, query, isSearching } = this.props;
return (
query && (
<React.Fragment>
<div className="search__results">
<section className="search__results-section">
<HiddenNsfwClaims uris={uris} />
{!isSearching && uris && uris.length ? (
uris.map(uri =>
parseURI(uri).claimName[0] === '@' ? (
<ChannelTile key={uri} uri={uri} />
) : (
<FileTile key={uri} uri={uri} />
)
)
) : (
<NoResults />
)}
</section>
</div>
</React.Fragment>
)
);
}
}
export default FileListSearch;

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectClaimIsMine } from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
import FileProperties from './view';
const select = (state, props) => ({
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
downloaded: !!makeSelectFileInfoForUri(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
});
export default connect(
select,
null
)(FileProperties);

View file

@ -0,0 +1,31 @@
// @flow
import * as icons from 'constants/icons';
import * as React from 'react';
import { parseURI } from 'lbry-redux';
import Icon from 'component/common/icon';
import FilePrice from 'component/filePrice';
type Props = {
uri: string,
downloaded: boolean,
claimIsMine: boolean,
isSubscribed: boolean,
isNew: boolean,
rewardedContentClaimIds: Array<string>,
};
export default function FileProperties(props: Props) {
const { uri, downloaded, claimIsMine, rewardedContentClaimIds, isSubscribed, isNew } = props;
const { claimId } = parseURI(uri);
const isRewardContent = rewardedContentClaimIds.includes(claimId);
return (
<div className="file-properties">
{isSubscribed && <Icon icon={icons.SUBSCRIPTION} />}
{!claimIsMine && downloaded && <Icon icon={icons.DOWNLOAD} />}
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{isNew && <span className="badge badge--alert">{__('NEW')}</span>}
<FilePrice hideFree uri={uri} />
</div>
);
}

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { makeSelectTagsForUri, selectFollowedTags } from 'lbry-redux';
import FileTags from './view';
const select = (state, props) => ({
tags: makeSelectTagsForUri(props.uri)(state),
followedTags: selectFollowedTags(state),
});
export default connect(
select,
null
)(FileTags);

View file

@ -0,0 +1,49 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
const MAX_TAGS = 4;
type Props = {
tags: Array<string>,
followedTags: Array<Tag>,
};
export default function FileTags(props: Props) {
const { tags, followedTags } = props;
let tagsToDisplay = [];
for (var i = 0; tagsToDisplay.length < MAX_TAGS - 2; i++) {
const tag = followedTags[i];
if (!tag) {
break;
}
if (tags.includes(tag.name)) {
tagsToDisplay.push(tag.name);
}
}
const sortedTags = tags.sort((a, b) => a.localeCompare(b));
for (var i = 0; i < sortedTags.length; i++) {
const tag = sortedTags[i];
if (!tag || tagsToDisplay.length === MAX_TAGS) {
break;
}
if (!tagsToDisplay.includes(tag)) {
tagsToDisplay.push(tag);
}
}
return (
<div className="file-properties">
{tagsToDisplay.map(tag => (
<Button key={tag} navigate={`$/tags?t=${tag}`} className="tag">
{tag}
</Button>
))}
</div>
);
}

View file

@ -1,43 +0,0 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectIsUriResolving,
makeSelectClaimIsMine,
makeSelectThumbnailForUri,
makeSelectTitleForUri,
makeSelectClaimIsNsfw,
} from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import { doClearPublish, doUpdatePublishForm } from 'redux/actions/publish';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
import FileTile from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
obscureNsfw: !selectShowNsfw(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
});
const perform = dispatch => ({
clearPublish: () => dispatch(doClearPublish()),
resolveUri: uri => dispatch(doResolveUri(uri)),
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
});
export default connect(
select,
perform
)(FileTile);

View file

@ -1,216 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text';
import Icon from 'component/common/icon';
import Button from 'component/button';
import classnames from 'classnames';
import FilePrice from 'component/filePrice';
import UriIndicator from 'component/uriIndicator';
import DateTime from 'component/dateTime';
import Yrbl from 'component/yrbl';
import { withRouter } from 'react-router-dom';
import { formatLbryUriForWeb } from 'util/uri';
type Props = {
obscureNsfw: boolean,
claimIsMine: boolean,
isDownloaded: boolean,
uri: string,
isResolvingUri: boolean,
rewardedContentClaimIds: Array<string>,
claim: ?StreamClaim,
metadata: ?StreamMetadata,
resolveUri: string => void,
clearPublish: () => void,
updatePublishForm: ({}) => void,
hideNoResult: boolean, // don't show the tile if there is no claim at this uri
displayHiddenMessage?: boolean,
size: string,
isSubscribed: boolean,
isNew: boolean,
history: { push: string => void },
thumbnail: ?string,
title: ?string,
nsfw: boolean,
};
class FileTile extends React.PureComponent<Props> {
static defaultProps = {
size: 'regular',
};
componentDidMount() {
const { isResolvingUri, claim, uri, resolveUri } = this.props;
if (!isResolvingUri && !claim && uri) resolveUri(uri);
}
componentDidUpdate() {
const { isResolvingUri, claim, uri, resolveUri } = this.props;
if (!isResolvingUri && claim === undefined && uri) resolveUri(uri);
}
renderFileProperties() {
const { isSubscribed, isDownloaded, claim, uri, rewardedContentClaimIds, isNew, claimIsMine } = this.props;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
if (!isNew && !isSubscribed && !isRewardContent && !isDownloaded) {
return null;
}
return (
// TODO: turn this into it's own component and share it with FileCard
// The only issue is icon placement on the search page
<div className="media__properties">
<FilePrice hideFree uri={uri} />
{isNew && <span className="badge badge--alert icon">{__('NEW')}</span>}
{isSubscribed && <Icon icon={ICONS.SUBSCRIPTION} />}
{isRewardContent && <Icon iconColor="red" icon={ICONS.FEATURED} />}
{!claimIsMine && isDownloaded && <Icon icon={ICONS.DOWNLOAD} />}
{claimIsMine && <Icon icon={ICONS.PUBLISHED} />}
</div>
);
}
render() {
const {
claim,
metadata,
isResolvingUri,
obscureNsfw,
claimIsMine,
clearPublish,
updatePublishForm,
hideNoResult,
displayHiddenMessage,
size,
history,
thumbnail,
title,
nsfw,
} = this.props;
if (!claim && isResolvingUri) {
return (
<div
className={classnames('media-tile media-placeholder', {
'media-tile--large': size === 'large',
})}
>
<div className="media__thumb placeholder" />
<div className="media__info">
<div className="media__title placeholder" />
<div className="media__channel placeholder" />
<div className="media__subtitle placeholder" />
</div>
</div>
);
}
const shouldHide = !claimIsMine && obscureNsfw && nsfw;
if (shouldHide) {
return displayHiddenMessage ? (
<span className="help">
{__('This file is hidden because it is marked NSFW. Update your')}{' '}
<Button button="link" navigate="/$/settings" label={__('content viewing preferences')} /> {__('to see it')}.
</span>
) : null;
}
const uri = normalizeURI(this.props.uri);
const isClaimed = !!claim;
const description = isClaimed && metadata && metadata.description ? metadata.description : '';
let height;
let name;
if (claim) {
({ name, height } = claim);
}
const wrapperProps = name
? {
onClick: () => history.push(formatLbryUriForWeb(uri)),
role: 'button',
}
: {};
return !name && hideNoResult ? null : (
<section
className={classnames('media-tile', {
'media-tile--small': size === 'small',
'media-tile--large': size === 'large',
'card--link': name,
})}
{...wrapperProps}
>
<CardMedia title={title || name} thumbnail={thumbnail} />
<div className="media__info">
{name && (
<Fragment>
<div className="media__title">
{(title || name) && <TruncatedText text={title || name} lines={size !== 'small' ? 1 : 2} />}
</div>
{size === 'small' && this.renderFileProperties()}
{size !== 'small' ? (
<div className="media__subtext">
{__('Published to')} <UriIndicator uri={uri} link /> <DateTime timeAgo uri={uri} />
</div>
) : (
<Fragment>
<div className="media__subtext">
<UriIndicator uri={uri} link />
<div>
<DateTime timeAgo block={height} />
</div>
</div>
</Fragment>
)}
</Fragment>
)}
{size !== 'small' && (
<div className="media__subtitle">
<TruncatedText text={description} lines={size === 'large' ? 4 : 3} />
</div>
)}
{size !== 'small' && this.renderFileProperties()}
{!name && (
<Yrbl
className="yrbl--search"
title={__("You get first dibs! There aren't any files here yet.")}
subtitle={
<Button
button="link"
label={
<Fragment>
{__('Publish something at')} {uri}
</Fragment>
}
onClick={e => {
// avoid navigating to /show from clicking on the section
e.stopPropagation();
// strip prefix from the Uri and use that as navigation parameter
const { claimName } = parseURI(uri);
clearPublish(); // to remove any existing publish data
updatePublishForm({ name: claimName }); // to populate the name
history.push('/$/publish');
}}
/>
}
/>
)}
</div>
</section>
);
}
}
export default withRouter(FileTile);

View file

@ -1,27 +0,0 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectUser } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetClientSetting } from 'redux/actions/settings';
import FirstRun from './view';
const select = state => ({
emailCollectionAcknowledged: makeSelectClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED)(state),
welcomeAcknowledged: makeSelectClientSetting(SETTINGS.NEW_USER_ACKNOWLEDGED)(state),
firstRunComplete: makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED)(state),
user: selectUser(state),
});
const perform = dispatch => ({
acknowledgeWelcome: () => {
dispatch(doSetClientSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, true));
},
completeFirstRun: () => {
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
},
});
export default connect(
select,
perform
)(FirstRun);

View file

@ -1,131 +0,0 @@
// @flow
import React, { PureComponent } from 'react';
import posed from 'react-pose';
import Button from 'component/button';
import EmailCollection from 'component/emailCollection';
import Yrbl from 'component/yrbl';
//
// Animation for items inside banner
// The height for items must be static (in banner.scss) so that we can reliably animate into the banner and be vertically centered
//
const spring = {
transition: {
duration: 250,
ease: 'easeOut',
},
};
const Welcome = posed.div({
hide: { opacity: 0, y: '310px', ...spring },
show: { opacity: 1, ...spring },
});
const Email = posed.div({
hide: { opacity: 0, y: '0', ...spring },
show: { opacity: 1, y: '-310px', ...spring, delay: 175 },
});
const Help = posed.div({
hide: { opacity: 0, y: '0', ...spring },
show: { opacity: 1, y: '-620px', ...spring, delay: 175 },
});
type Props = {
welcomeAcknowledged: boolean,
emailCollectionAcknowledged: boolean,
firstRunComplete: boolean,
acknowledgeWelcome: () => void,
completeFirstRun: () => void,
};
export default class FirstRun extends PureComponent<Props> {
getWelcomeMessage() {
// @if TARGET='app'
const message = (
<React.Fragment>
<p>
{__('Using LBRY is like dating a centaur. Totally normal up top, and')} <em>{__('way different')}</em>{' '}
{__('underneath.')}
</p>
<p>{__('Up top, LBRY is similar to popular media sites.')}</p>
<p>{__('Below, LBRY is controlled by users -- you -- via blockchain and decentralization.')}</p>
</React.Fragment>
);
// @endif
// @if TARGET='web'
// $FlowFixMe
const message = (
<React.Fragment>
<p>{__('Thanks for trying out lbry.tv')}</p>
<p>
{__(
'Some features are only available on our desktop app. We are working hard to add them here. Check back later or download the app.'
)}
</p>
</React.Fragment>
);
// @endif
return message;
}
render() {
const {
welcomeAcknowledged,
emailCollectionAcknowledged,
firstRunComplete,
acknowledgeWelcome,
completeFirstRun,
} = this.props;
if (firstRunComplete) {
return null;
}
const showWelcome = !welcomeAcknowledged;
const showEmail = !emailCollectionAcknowledged && welcomeAcknowledged;
const showHelp = !showWelcome && !showEmail;
return (
<div className="banner banner--first-run">
<Yrbl className="yrbl--first-run" />
<div className="banner__item">
<div className="banner__item--static-for-animation">
<Welcome className="banner__content" pose={showWelcome ? 'show' : 'hide'}>
<div>
<header className="card__header">
<h1 className="card__title">{__('Hi There')}</h1>
</header>
<div className="card__content">
{this.getWelcomeMessage()}
<div className="card__actions card__actions--top-space">
<Button button="primary" onClick={acknowledgeWelcome} label={__("I'm In")} />
</div>
</div>
</div>
</Welcome>
</div>
<div className="banner__item--static-for-animation">
<Email pose={showEmail ? 'show' : 'hide'}>
<EmailCollection />
</Email>
</div>
<div className="banner__item--static-for-animation">
<Help pose={showHelp ? 'show' : 'hide'}>
<header className="card__header">
<h1 className="card__title">{__('You Are Awesome!')}</h1>
</header>
<div className="card__content">
<p>{__("Check out some of the neat content below me. I'll see you around!")}</p>
<div className="card__actions">
<Button button="primary" onClick={completeFirstRun} label={__('Lets Get Started')} />
</div>
</div>
</Help>
</div>
</div>
</div>
);
}
}

View file

@ -4,42 +4,29 @@ import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
import WunderBar from 'component/wunderbar'; import WunderBar from 'component/wunderbar';
import Icon from 'component/common/icon';
type Props = { type Props = {
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
balance: string, balance: string,
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
roundedBalance: string, roundedBalance: number,
isBackDisabled: boolean,
isForwardDisabled: boolean,
back: () => void,
forward: () => void,
downloadUpgradeRequested: any => void, downloadUpgradeRequested: any => void,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
const { const { autoUpdateDownloaded, downloadUpgradeRequested, isUpgradeAvailable, roundedBalance } = props;
autoUpdateDownloaded,
balance,
downloadUpgradeRequested,
isUpgradeAvailable,
roundedBalance,
back,
isBackDisabled,
forward,
isForwardDisabled,
} = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable); const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
return ( return (
<header className="header"> <header className="header">
<div className="title-bar" />
<div className="header__contents">
<div className="header__navigation"> <div className="header__navigation">
<Button <Button
className="header__navigation-item header__navigation-item--lbry" className="header__navigation-item header__navigation-item--lbry"
label={__('LBRY')} label={__('LBRY')}
iconRight={ICONS.LBRY} icon={ICONS.LBRY}
navigate="/" navigate="/"
/> />
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
@ -66,23 +53,19 @@ const Header = (props: Props) => {
<WunderBar /> <WunderBar />
<div className="header__navigation"> <div className="header__navigation">
<Button
className="header__navigation-item header__navigation-item--menu"
description={__('Menu')}
icon={ICONS.MENU}
iconSize={15}
/>
<Button <Button
className="header__navigation-item header__navigation-item--right-action" className="header__navigation-item header__navigation-item--right-action"
activeClass="header__navigation-item--active" activeClass="header__navigation-item--active"
description={__('Your wallet')}
title={`Your balance is ${balance} LBRY Credits`}
label={ label={
roundedBalance > 0 ? (
<React.Fragment> <React.Fragment>
{roundedBalance} <LbcSymbol /> {roundedBalance} <LbcSymbol />
</React.Fragment> </React.Fragment>
) : (
__('Account')
)
} }
icon={ICONS.ACCOUNT}
navigate="/$/account" navigate="/$/account"
/> />
@ -92,12 +75,10 @@ const Header = (props: Props) => {
description={__('Publish content')} description={__('Publish content')}
icon={ICONS.UPLOAD} icon={ICONS.UPLOAD}
iconSize={24} iconSize={24}
label={isUpgradeAvailable ? '' : __('Publish')}
navigate="/$/publish" navigate="/$/publish"
/> />
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
{showUpgradeButton && ( {showUpgradeButton && (
<Button <Button
className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade" className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade"
@ -108,6 +89,15 @@ const Header = (props: Props) => {
/> />
)} )}
{/* @endif */} {/* @endif */}
<Button
className="header__navigation-item header__navigation-item--right-action"
activeClass="header__navigation-item--active"
icon={ICONS.SETTINGS}
iconSize={24}
navigate="/$/settings"
/>
</div>
</div> </div>
</header> </header>
); );

View file

@ -23,7 +23,7 @@ export default function NavigationHistoryRecent(props: Props) {
))} ))}
</section> </section>
<div className="card__actions"> <div className="card__actions">
<Button navigate="/$/history/all" button="link" label={__('See All Visited Links')} /> <Button navigate="/$/library/all" button="link" label={__('See All Visited Links')} />
</div> </div>
</div> </div>
) : null; ) : null;

View file

@ -9,7 +9,6 @@ const LOADER_TIMEOUT = 1000;
type Props = { type Props = {
children: React.Node | Array<React.Node>, children: React.Node | Array<React.Node>,
pageTitle: ?string, pageTitle: ?string,
notContained: ?boolean, // No max-width, but keep the padding
loading: ?boolean, loading: ?boolean,
className: ?string, className: ?string,
}; };
@ -69,16 +68,11 @@ class Page extends React.PureComponent<Props, State> {
loaderTimeout: ?TimeoutID; loaderTimeout: ?TimeoutID;
render() { render() {
const { children, notContained, loading, className } = this.props; const { children, loading, className } = this.props;
const { showLoader } = this.state; const { showLoader } = this.state;
return ( return (
<main <main className={classnames('main', className)}>
className={classnames('main', className, {
'main--contained': !notContained,
'main--not-contained': notContained,
})}
>
{!loading && children} {!loading && children}
{showLoader && ( {showLoader && (
<div className="main--empty"> <div className="main--empty">

View file

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import FileTile from 'component/fileTile'; import FileList from 'component/fileList';
type Props = { type Props = {
uri: string, uri: string,
@ -51,15 +51,14 @@ export default class RecommendedContent extends React.PureComponent<Props> {
const { recommendedContent, isSearching } = this.props; const { recommendedContent, isSearching } = this.props;
return ( return (
<section className="media-group--list-recommended"> <section className="card">
<span>Related</span> <FileList
{recommendedContent && slim
recommendedContent.map(recommendedUri => ( loading={isSearching}
<FileTile hideNoResult size="small" key={recommendedUri} uri={recommendedUri} /> uris={recommendedContent}
))} header={<span>Related</span>}
{recommendedContent && !recommendedContent.length && !isSearching && ( empty={<div className="empty">{__('No related content found')}</div>}
<div className="media__subtitle">No related content found</div> />
)}
</section> </section>
); );
} }

View file

@ -43,7 +43,8 @@ class RewardSummary extends React.Component<Props> {
<React.Fragment> <React.Fragment>
{__('There are no rewards available at this time, please check back later')}. {__('There are no rewards available at this time, please check back later')}.
</React.Fragment> </React.Fragment>
))} ))}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />.
</p> </p>
</header> </header>
@ -55,11 +56,6 @@ class RewardSummary extends React.Component<Props> {
label={hasRewards ? __('Claim Rewards') : __('View Rewards')} label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
/> />
</div> </div>
<p className="help">
{__('Read our')} <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/rewards" />{' '}
{__('to learn more about LBRY Rewards')}.
</p>
</div> </div>
</section> </section>
); );

View file

@ -19,6 +19,8 @@ import SearchPage from 'page/search';
import UserHistoryPage from 'page/userHistory'; import UserHistoryPage from 'page/userHistory';
import SendCreditsPage from 'page/sendCredits'; import SendCreditsPage from 'page/sendCredits';
import NavigationHistory from 'page/navigationHistory'; import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags';
import TagsEditPage from 'page/tagsEdit';
const Scroll = withRouter(function ScrollWrapper(props) { const Scroll = withRouter(function ScrollWrapper(props) {
const { pathname } = props.location; const { pathname } = props.location;
@ -50,11 +52,12 @@ export default function AppRouter() {
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} /> <Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} /> <Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} /> <Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} /> <Route path={`/$/${PAGES.LIBRARY}`} exact component={UserHistoryPage} />
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} /> <Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
<Route path={`/$/${PAGES.SEND}`} exact component={SendCreditsPage} /> <Route path={`/$/${PAGES.SEND}`} exact component={SendCreditsPage} />
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} /> <Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
<Route path={`/$/${PAGES.HISTORY}/all`} exact component={NavigationHistory} /> <Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.TAGS}/edit`} exact component={TagsEditPage} />
{/* 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="/:claimName" exact component={ShowPage} /> <Route path="/:claimName" exact component={ShowPage} />

View file

@ -1,8 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions, doToast } from 'lbry-redux'; import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions } from 'lbry-redux';
import { doToggleSearchExpanded } from 'redux/actions/app'; import { doToggleSearchExpanded } from 'redux/actions/app';
import { selectSearchOptionsExpanded } from 'redux/selectors/app'; import { selectSearchOptionsExpanded } from 'redux/selectors/app';
import analytics from 'analytics';
import SearchOptions from './view'; import SearchOptions from './view';
const select = state => ({ const select = state => ({
@ -14,24 +13,6 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })), setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
toggleSearchExpanded: () => dispatch(doToggleSearchExpanded()), toggleSearchExpanded: () => dispatch(doToggleSearchExpanded()),
onFeedbackPositive: query => {
analytics.apiSearchFeedback(query, 1);
dispatch(
doToast({
message: __('Thanks for the feedback! You help make the app better for everyone.'),
})
);
},
onFeedbackNegative: query => {
analytics.apiSearchFeedback(query, 0);
dispatch(
doToast({
message: __(
'Thanks for the feedback. Mark has been notified and is currently walking over to his computer to work on this.'
),
})
);
},
}); });
export default connect( export default connect(

View file

@ -16,39 +16,20 @@ type Props = {
options: {}, options: {},
expanded: boolean, expanded: boolean,
toggleSearchExpanded: () => void, toggleSearchExpanded: () => void,
query: string,
onFeedbackPositive: string => void,
onFeedbackNegative: string => void,
}; };
const SearchOptions = (props: Props) => { const SearchOptions = (props: Props) => {
const { const { options, setSearchOption, expanded, toggleSearchExpanded } = props;
options,
setSearchOption,
expanded,
toggleSearchExpanded,
query,
onFeedbackPositive,
onFeedbackNegative,
} = props;
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT]; const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
return ( return (
<div className="search__options-wrapper"> <div>
<div className="card--space-between">
<Button <Button
button="alt" button="alt"
label={__('FILTER')} label={__('FILTER')}
iconRight={expanded ? ICONS.UP : ICONS.DOWN} iconRight={expanded ? ICONS.UP : ICONS.DOWN}
onClick={toggleSearchExpanded} onClick={toggleSearchExpanded}
/> />
<div className="media__action-group">
<span>{__('Find what you were looking for?')}</span>
<Button button="alt" description={__('Yes')} onClick={() => onFeedbackPositive(query)} icon={ICONS.YES} />
<Button button="alt" description={__('No')} onClick={() => onFeedbackNegative(query)} icon={ICONS.NO} />
</div>
</div>
<ExpandableOptions pose={expanded ? 'show' : 'hide'}> <ExpandableOptions pose={expanded ? 'show' : 'hide'}>
{expanded && ( {expanded && (
<Form className="card__content search__options"> <Form className="card__content search__options">

View file

@ -1,14 +1,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectUnreadAmount } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectShouldShowInviteGuide } from 'redux/selectors/app'; import { selectFollowedTags } from 'lbry-redux';
import SideBar from './view'; import SideBar from './view';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { SETTINGS } from 'lbry-redux'; import { SETTINGS } from 'lbry-redux';
const select = state => ({ const select = state => ({
unreadSubscriptionTotal: selectUnreadAmount(state), subscriptions: selectSubscriptions(state),
followedTags: selectFollowedTags(state),
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
shouldShowInviteGuide: selectShouldShowInviteGuide(state),
}); });
const perform = () => ({}); const perform = () => ({});

View file

@ -3,17 +3,15 @@ import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as React from 'react'; import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames'; import Tag from 'component/tag';
import Tooltip from 'component/common/tooltip';
type Props = { type Props = {
unreadSubscriptionTotal: number, subscriptions: Array<Subscription>,
shouldShowInviteGuide: string, followedTags: Array<Tag>,
}; };
class SideBar extends React.PureComponent<Props> { function SideBar(props: Props) {
render() { const { subscriptions, followedTags } = props;
const { unreadSubscriptionTotal, shouldShowInviteGuide } = this.props;
const buildLink = (path, label, icon, guide) => ({ const buildLink = (path, label, icon, guide) => ({
navigate: path ? `$/${path}` : '/', navigate: path ? `$/${path}` : '/',
label, label,
@ -21,89 +19,58 @@ class SideBar extends React.PureComponent<Props> {
guide, guide,
}); });
const renderLink = (linkProps, index) => { const renderLink = linkProps => (
const { guide } = linkProps; <Button {...linkProps} key={linkProps.label} className="navigation__link" activeClass="navigation__link--active" />
const inner = (
<Button
{...linkProps}
className={classnames('navigation__link', {
'navigation__link--guide': guide,
})}
activeClass="navigation__link--active"
/>
); );
return ( return (
<li key={index}> <div className="navigation-wrapper">
{guide ? (
<Tooltip key={guide} alwaysVisible direction="right" body={guide}>
{inner}
</Tooltip>
) : (
inner
)}
</li>
);
};
return (
<nav className="navigation"> <nav className="navigation">
<ul className="navigation__links"> <ul className="navigation__links">
{[ {[
{ {
...buildLink(null, __('Discover'), ICONS.DISCOVER), ...buildLink(null, __('Home'), ICONS.HOME),
}, },
{ {
...buildLink( ...buildLink(PAGES.SUBSCRIPTIONS, __('Subscriptions'), ICONS.SUBSCRIPTION),
PAGES.SUBSCRIPTIONS,
`${__('Subscriptions')} ${unreadSubscriptionTotal > 0 ? '(' + unreadSubscriptionTotal + ')' : ''}`,
ICONS.SUBSCRIPTION
),
}, },
{ {
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED), ...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
}, },
{ {
...buildLink(PAGES.HISTORY, __('Library'), ICONS.DOWNLOAD), ...buildLink(PAGES.LIBRARY, __('Library'), ICONS.DOWNLOAD),
},
].map(renderLink)}
</ul>
<div className="navigation__link navigation__link--title">Account</div>
<ul className="navigation__links">
{[
{
...buildLink(PAGES.ACCOUNT, __('Overview'), ICONS.ACCOUNT),
},
{
...buildLink(PAGES.INVITE, __('Invite'), ICONS.INVITE, shouldShowInviteGuide && __('Check this out!')),
},
{
...buildLink(PAGES.REWARDS, __('Rewards'), ICONS.FEATURED),
},
{
...buildLink(PAGES.SEND, __('Send & Recieve'), ICONS.SEND),
},
{
...buildLink(PAGES.TRANSACTIONS, __('Transactions'), ICONS.TRANSACTIONS),
},
{
...buildLink(PAGES.SETTINGS, __('Settings'), ICONS.SETTINGS),
}, },
].map(renderLink)} ].map(renderLink)}
</ul> </ul>
<ul className="navigation__links navigation__links--bottom"> <Button
{[ navigate="/$/tags/edit"
{ iconRight={ICONS.SETTINGS}
...buildLink(PAGES.HELP, __('Help'), ICONS.HELP), className="navigation__link--title navigation__link"
}, activeClass="navigation__link--active"
].map(renderLink)} label={__('Following')}
/>
<ul className="tags--vertical navigation__links">
{followedTags.map(({ name }, key) => (
<li key={name}>
<Tag navigate={`/$/tags?t${name}`} name={name} />
</li>
))}
</ul>
<ul className="navigation__links--small">
{subscriptions.map(({ uri, channelName }) => (
<Button
key={uri}
navigate={uri}
label={channelName}
className="navigation__link"
activeClass="navigation__link--active"
/>
))}
</ul> </ul>
</nav> </nav>
</div>
); );
} }
}
export default SideBar; export default SideBar;

View file

@ -66,8 +66,8 @@ class Spinner extends PureComponent<Props, State> {
className={classnames('spinner', { className={classnames('spinner', {
'spinner--dark': !light && (dark || theme === LIGHT_THEME), 'spinner--dark': !light && (dark || theme === LIGHT_THEME),
'spinner--light': !dark && (light || theme === DARK_THEME), 'spinner--light': !dark && (light || theme === DARK_THEME),
'spinner--splash': type === 'splash',
'spinner--small': type === 'small', 'spinner--small': type === 'small',
'spinner--splash': type === 'splash',
})} })}
> >
<div className="rect rect1" /> <div className="rect rect1" />

View file

@ -22,7 +22,7 @@ type Props = {
buttonStyle: string, buttonStyle: string,
}; };
export default (props: Props) => { export default function SubscribeButton(props: Props) {
const { const {
uri, uri,
doChannelSubscribe, doChannelSubscribe,
@ -36,14 +36,14 @@ export default (props: Props) => {
} = props; } = props;
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe; const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe'); const subscriptionLabel = isSubscribed ? __('Subscribed') : __('Subscribe');
const { claimName } = parseURI(uri); const { claimName } = parseURI(uri);
return ( return (
<Button <Button
iconColor="red" iconColor="red"
icon={isSubscribed ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIPTION} icon={ICONS.SUBSCRIPTION}
button={buttonStyle || 'alt'} button={buttonStyle || 'alt'}
label={subscriptionLabel} label={subscriptionLabel}
onClick={e => { onClick={e => {
@ -64,4 +64,4 @@ export default (props: Props) => {
}} }}
/> />
); );
}; }

View file

@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { selectSuggestedChannels, selectIsFetchingSuggested } from 'redux/selectors/subscriptions';
import SuggestedSubscriptions from './view';
const select = state => ({
suggested: selectSuggestedChannels(state),
loading: selectIsFetchingSuggested(state),
});
export default connect(
select,
null
)(SuggestedSubscriptions);

View file

@ -1,38 +0,0 @@
// @flow
import React, { Component } from 'react';
import CategoryList from 'component/categoryList';
import Spinner from 'component/spinner';
type Props = {
suggested: ?Array<{ label: string, uri: string }>,
loading: boolean,
};
class SuggestedSubscriptions extends Component<Props> {
shouldComponentUpdate(nextProps: Props) {
const { suggested } = this.props;
return !suggested && !!nextProps.suggested;
}
render() {
const { suggested, loading } = this.props;
if (loading) {
return (
<div className="main--empty">
<Spinner delayed />
</div>
);
}
return suggested ? (
<div className="card__content subscriptions__suggested main__item--extend-outside">
{suggested.map(({ uri, label }) => (
<CategoryList key={uri} category={label} categoryLink={uri} />
))}
</div>
) : null;
}
}
export default SuggestedSubscriptions;

View file

@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import Tag from './view';
const select = state => ({});
const perform = () => ({});
export default connect(
select,
perform
)(Tag);

View file

@ -0,0 +1,35 @@
// @flow
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import classnames from 'classnames';
import Icon from 'component/common/icon';
import Button from 'component/button';
type Props = {
name: string,
type?: string,
onClick?: any => any,
};
export default function Tag(props: Props) {
const { name, type, onClick } = props;
const clickProps = onClick ? { onClick } : { navigate: `/$/tags?t=${name}` };
return (
<Button
{...clickProps}
className={classnames('tag', {
'tag--add': type === 'add',
'tag--remove': type === 'remove',
})}
label={
<Fragment>
{name}
{type && <Icon className="tag__action-label" icon={type === 'remove' ? ICONS.CLOSE : ICONS.ADD} />}
</Fragment>
}
/>
);
}

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import {
selectUnfollowedTags,
selectFollowedTags,
doReplaceTags,
doToggleTagFollow,
doAddTag,
doDeleteTag,
} from 'lbry-redux';
import DiscoveryFirstRun from './view';
const select = (state, props) => ({
unfollowedTags: selectUnfollowedTags(state),
followedTags: selectFollowedTags(state),
});
export default connect(
select,
{
doToggleTagFollow,
doAddTag,
doDeleteTag,
doReplaceTags,
}
)(DiscoveryFirstRun);

View file

@ -0,0 +1,69 @@
// @flow
import React, { useState } from 'react';
import { useTransition, animated } from 'react-spring';
import { Form, FormField } from 'component/common/form';
import Tag from 'component/tag';
const unfollowedTagsAnimation = {
from: { opacity: 0 },
enter: { opacity: 1, maxWidth: 200 },
leave: { opacity: 0, maxWidth: 0 },
};
type Props = {
unfollowedTags: Array<Tag>,
followedTags: Array<Tag>,
doToggleTagFollow: string => void,
doAddTag: string => void,
};
export default function TagSelect(props: Props) {
const { unfollowedTags, followedTags, doToggleTagFollow, doAddTag } = props;
const [newTag, setNewTag] = useState('');
let tags = unfollowedTags.slice();
if (newTag) {
tags = [{ name: newTag }, ...tags];
}
const suggestedTags = tags
.filter(({ name }) => (newTag ? name.toLowerCase().includes(newTag.toLowerCase()) : true))
.slice(0, 5);
const suggestedTransitions = useTransition(suggestedTags, tag => tag.name, unfollowedTagsAnimation);
function onChange(e) {
setNewTag(e.target.value);
}
function handleSubmit() {
setNewTag('');
if (!unfollowedTags.includes(newTag)) {
doAddTag(newTag);
}
if (!followedTags.includes(newTag)) {
doToggleTagFollow(newTag);
}
}
return (
<div>
<Form onSubmit={handleSubmit}>
<FormField
label={__('Tags')}
onChange={onChange}
placeholder={__('Search for more tags')}
type="text"
value={newTag}
/>
</Form>
<ul className="tags">
{suggestedTransitions.map(({ item, key, props }) => (
<animated.li key={key} style={props}>
<Tag name={item.name} type="add" onClick={() => doToggleTagFollow(item.name)} />
</animated.li>
))}
{!suggestedTransitions.length && <p className="empty tags__empty-message">No suggested tags</p>}
</ul>
</div>
);
}

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import {
selectUnfollowedTags,
selectFollowedTags,
doReplaceTags,
doToggleTagFollow,
doAddTag,
doDeleteTag,
} from 'lbry-redux';
import DiscoveryFirstRun from './view';
const select = (state, props) => ({
unfollowedTags: selectUnfollowedTags(state),
followedTags: selectFollowedTags(state),
});
export default connect(
select,
{
doToggleTagFollow,
doAddTag,
doDeleteTag,
doReplaceTags,
}
)(DiscoveryFirstRun);

View file

@ -0,0 +1,63 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import Button from 'component/button';
import Tag from 'component/tag';
import TagsSearch from 'component/tagsSearch';
import usePersistedState from 'util/use-persisted-state';
import { useTransition, animated } from 'react-spring';
type Props = {
followedTags: Array<Tag>,
showClose: boolean,
title: string,
doDeleteTag: string => void,
};
const tagsAnimation = {
from: { opacity: 0 },
enter: { opacity: 1, maxWidth: 400 },
leave: { opacity: 0, maxWidth: 0 },
};
export default function TagSelect(props: Props) {
const { title, followedTags, showClose, doDeleteTag } = props;
const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false);
function handleClose() {
setHasClosed(true);
}
const transitions = useTransition(followedTags.map(tag => tag), tag => tag.name, tagsAnimation);
return (
((showClose && !hasClosed) || !showClose) && (
<div className="card--section">
<h2 className="card__title card__title--flex-between">
{title}
{showClose && !hasClosed && <Button button="close" icon={ICONS.CLOSE} onClick={handleClose} />}
</h2>
<div className="card__content">
<ul className="tags--remove">
{transitions.map(({ item, props, key }) => (
<animated.li key={key} style={props}>
<Tag
name={item.name}
type="remove"
onClick={() => {
doDeleteTag(item.name);
}}
/>
</animated.li>
))}
{!transitions.length && (
<div className="card__subtitle">{__("You aren't following any tags, try searching for one.")}</div>
)}
</ul>
<TagsSearch />
</div>
</div>
)
);
}

View file

@ -41,7 +41,7 @@ class TransactionListRecent extends React.PureComponent<Props> {
{!fetchingTransactions && !hasTransactions && ( {!fetchingTransactions && !hasTransactions && (
<div className="card__content"> <div className="card__content">
<p className="card__subtitle">{__('No transactions... yet.')}</p> <p className="card__subtitle">{__('No transactions.')}</p>
</div> </div>
)} )}

View file

@ -7,8 +7,7 @@ type Props = {
isResolvingUri: boolean, isResolvingUri: boolean,
channelUri: ?string, channelUri: ?string,
link: ?boolean, link: ?boolean,
claim: ?StreamClaim, claim: ?Claim,
channelClaim: ?ChannelClaim,
// Lint thinks we aren't using these, even though we are. // Lint thinks we aren't using these, even though we are.
// Possibly because the resolve function is an arrow function that is passed in props? // Possibly because the resolve function is an arrow function that is passed in props?
resolveUri: string => void, resolveUri: string => void,
@ -16,12 +15,12 @@ type Props = {
}; };
class UriIndicator extends React.PureComponent<Props> { class UriIndicator extends React.PureComponent<Props> {
componentWillMount() { componentDidMount() {
this.resolve(this.props); this.resolve(this.props);
} }
componentWillReceiveProps(nextProps: Props) { componentDidUpdate() {
this.resolve(nextProps); this.resolve(this.props);
} }
resolve = (props: Props) => { resolve = (props: Props) => {
@ -39,11 +38,16 @@ class UriIndicator extends React.PureComponent<Props> {
return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>; return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
} }
if (!claim.signing_channel) { const isChannelClaim = claim.value_type === 'channel';
if (!claim.signing_channel && !isChannelClaim) {
return <span className="channel-name">Anonymous</span>; return <span className="channel-name">Anonymous</span>;
} }
const { name, claim_id: claimId } = claim.signing_channel; const channelClaim = isChannelClaim ? claim : claim.signing_channel;
if (channelClaim) {
const { name, claim_id: claimId } = channelClaim;
let channelLink; let channelLink;
if (claim.is_channel_signature_valid) { if (claim.is_channel_signature_valid) {
channelLink = link ? buildURI({ channelName: name, claimId }) : false; channelLink = link ? buildURI({ channelName: name, claimId }) : false;
@ -60,6 +64,9 @@ class UriIndicator extends React.PureComponent<Props> {
{inner} {inner}
</Button> </Button>
); );
} else {
return null;
}
} }
} }

View file

@ -72,7 +72,6 @@ class UserEmailNew extends React.PureComponent<Props, State> {
/> />
</Form> </Form>
<div className="card__actions">{cancelButton}</div> <div className="card__actions">{cancelButton}</div>
<p className="help">{__('Your email address will never be sold and you can unsubscribe at any time.')}</p>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -19,7 +19,11 @@ const WalletBalance = (props: Props) => {
</header> </header>
<div className="card__content"> <div className="card__content">
<h3>{__('You currently have')}</h3> <h3>{__('You currently have')}</h3>
{(balance || balance === 0) && <CreditAmount large badge={false} amount={balance} precision={8} />} {(balance || balance === 0) && (
<span className="card__content--large">
<CreditAmount badge={false} amount={balance} precision={8} />
</span>
)}
</div> </div>
</section> </section>
); );

View file

@ -18,7 +18,6 @@ export const SHOW_MODAL = 'SHOW_MODAL';
export const HIDE_MODAL = 'HIDE_MODAL'; export const HIDE_MODAL = 'HIDE_MODAL';
export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED'; export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED';
export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED'; export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED';
export const ENNNHHHAAANNNCEEE = 'ENNNHHHAAANNNCEEE';
// Navigation // Navigation
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH'; export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';
@ -193,8 +192,6 @@ export const SET_VIEW_MODE = 'SET_VIEW_MODE';
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START'; export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS'; export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL'; export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
export const SUBSCRIPTION_FIRST_RUN_COMPLETED = 'SUBSCRIPTION_FIRST_RUN_COMPLETED';
export const VIEW_SUGGESTED_SUBSCRIPTIONS = 'VIEW_SUGGESTED_SUBSCRIPTIONS';
// Publishing // Publishing
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH'; export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';

View file

@ -13,6 +13,7 @@ export const DOWNLOAD = 'Download';
export const UPLOAD = 'UploadCloud'; export const UPLOAD = 'UploadCloud';
export const PUBLISHED = 'Cloud'; export const PUBLISHED = 'Cloud';
export const CLOSE = 'X'; export const CLOSE = 'X';
export const ADD = 'Plus';
export const EDIT = 'Edit3'; export const EDIT = 'Edit3';
export const DELETE = 'Trash'; export const DELETE = 'Trash';
export const REPORT = 'Flag'; export const REPORT = 'Flag';

View file

@ -4,7 +4,7 @@ export const CHANNEL = 'channel';
export const DISCOVER = 'discover'; export const DISCOVER = 'discover';
export const DOWNLOADED = 'downloaded'; export const DOWNLOADED = 'downloaded';
export const HELP = 'help'; export const HELP = 'help';
export const HISTORY = 'history'; export const LIBRARY = 'library';
export const INVITE = 'invite'; export const INVITE = 'invite';
export const PUBLISH = 'publish'; export const PUBLISH = 'publish';
export const PUBLISHED = 'published'; export const PUBLISHED = 'published';
@ -18,3 +18,4 @@ export const ACCOUNT = 'account';
export const SUBSCRIPTIONS = 'subscriptions'; export const SUBSCRIPTIONS = 'subscriptions';
export const SEARCH = 'search'; export const SEARCH = 'search';
export const TRANSACTIONS = 'transactions'; export const TRANSACTIONS = 'transactions';
export const TAGS = 'tags';

View file

@ -3,7 +3,6 @@
export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged'; export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged'; export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged'; export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
export const FIRST_RUN_COMPLETED = 'first_run_completed';
export const INVITE_ACKNOWLEDGED = 'invite_acknowledged'; export const INVITE_ACKNOWLEDGED = 'invite_acknowledged';
export const LANGUAGE = 'language'; export const LANGUAGE = 'language';
export const SHOW_NSFW = 'showNsfw'; export const SHOW_NSFW = 'showNsfw';

View file

@ -3,7 +3,6 @@
import * as React from 'react'; import * as React from 'react';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import Button from 'component/button'; import Button from 'component/button';
import app from 'app';
import classnames from 'classnames'; import classnames from 'classnames';
type ModalProps = { type ModalProps = {
@ -20,7 +19,6 @@ type ModalProps = {
extraContent?: React.Node, extraContent?: React.Node,
expandButtonLabel?: string, expandButtonLabel?: string,
hideButtonLabel?: string, hideButtonLabel?: string,
fullScreen: boolean,
title?: string | React.Node, title?: string | React.Node,
}; };
@ -32,7 +30,6 @@ export class Modal extends React.PureComponent<ModalProps> {
abortButtonLabel: __('Cancel'), abortButtonLabel: __('Cancel'),
confirmButtonDisabled: false, confirmButtonDisabled: false,
abortButtonDisabled: false, abortButtonDisabled: false,
fullScreen: false,
}; };
render() { render() {
@ -45,7 +42,6 @@ export class Modal extends React.PureComponent<ModalProps> {
abortButtonLabel, abortButtonLabel,
abortButtonDisabled, abortButtonDisabled,
onAborted, onAborted,
fullScreen,
className, className,
title, title,
...modalProps ...modalProps
@ -54,10 +50,7 @@ export class Modal extends React.PureComponent<ModalProps> {
<ReactModal <ReactModal
{...modalProps} {...modalProps}
onRequestClose={onAborted || onConfirmed} onRequestClose={onAborted || onConfirmed}
className={classnames('card', className, { className={classnames('card card--modal modal', className)}
modal: !fullScreen,
'modal--fullscreen': fullScreen,
})}
overlayClassName="modal-overlay" overlayClassName="modal-overlay"
> >
{title && ( {title && (

View file

@ -40,7 +40,7 @@ class ModalAffirmPurchase extends React.PureComponent<Props> {
onAborted={cancelPurchase} onAborted={cancelPurchase}
> >
<section className="card__content"> <section className="card__content">
<p> <p className="card__subtitle">
{__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '} {__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '}
<strong> <strong>
<FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} /> <FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} />

View file

@ -50,8 +50,6 @@ function ModalAutoGenerateThumbnail(props: Props) {
return; return;
} }
console.log('resized');
const fixedWidth = 450; const fixedWidth = 450;
const videoWidth = player.videoWidth; const videoWidth = player.videoWidth;
const videoHeight = player.videoHeight; const videoHeight = player.videoHeight;

View file

@ -16,6 +16,8 @@ const WalletPage = () => (
<WalletBalance /> <WalletBalance />
<RewardSummary /> <RewardSummary />
</div> </div>
<WalletAddress />
<WalletSend />
<TransactionListRecent /> <TransactionListRecent />
</div> </div>
</Page> </Page>

View file

@ -49,12 +49,13 @@ function ChannelPage(props: Props) {
}; };
return ( return (
<Page notContained className="main--no-padding-top"> <Page>
<header className="channel-cover main__item--extend-outside"> <div className="card">
<header className="channel-cover">
{cover && <img className="channel-cover__custom" src={cover} />} {cover && <img className="channel-cover__custom" src={cover} />}
<div className="channel__primary-info"> <div className="channel__primary-info">
<ChannelThumbnail uri={uri} /> <ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} />
<div> <div>
<h1 className="channel__title">{title || channelName}</h1> <h1 className="channel__title">{title || channelName}</h1>
@ -67,7 +68,7 @@ function ChannelPage(props: Props) {
</header> </header>
<Tabs onChange={onTabChange} index={tabIndex}> <Tabs onChange={onTabChange} index={tabIndex}>
<TabList className="main__item--extend-outside tabs__list--channel-page"> <TabList className="tabs__list--channel-page">
<Tab>{__('Content')}</Tab> <Tab>{__('Content')}</Tab>
<Tab>{__('About')}</Tab> <Tab>{__('About')}</Tab>
<div className="card__actions"> <div className="card__actions">
@ -76,7 +77,7 @@ function ChannelPage(props: Props) {
</div> </div>
</TabList> </TabList>
<TabPanels> <TabPanels className="channel__data">
<TabPanel> <TabPanel>
<ChannelContent uri={uri} /> <ChannelContent uri={uri} />
</TabPanel> </TabPanel>
@ -85,6 +86,7 @@ function ChannelPage(props: Props) {
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>
</div>
</Page> </Page>
); );
} }

View file

@ -1,23 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { selectFollowedTags } from 'lbry-redux';
doFetchRewardedContent,
doRewardList,
selectFeaturedUris,
doFetchFeaturedUris,
selectFetchingFeaturedUris,
} from 'lbryinc';
import DiscoverPage from './view'; import DiscoverPage from './view';
const select = state => ({ const select = state => ({
featuredUris: selectFeaturedUris(state), followedTags: selectFollowedTags(state),
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
}); });
const perform = dispatch => ({ const perform = {};
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchRewards: () => dispatch(doRewardList()),
});
export default connect( export default connect(
select, select,

View file

@ -1,82 +1,24 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import FileListDiscover from 'component/fileListDiscover';
import TagsSelect from 'component/tagsSelect';
import Page from 'component/page'; import Page from 'component/page';
import CategoryList from 'component/categoryList';
import FirstRun from 'component/firstRun';
type Props = { type Props = {
fetchFeaturedUris: () => void, followedTags: Array<Tag>,
fetchRewardedContent: () => void,
fetchRewards: () => void,
fetchingFeaturedUris: boolean,
featuredUris: {},
}; };
class DiscoverPage extends React.PureComponent<Props> { function DiscoverPage(props: Props) {
constructor() { const { followedTags } = props;
super();
this.continousFetch = undefined;
}
componentDidMount() {
const { fetchFeaturedUris, fetchRewardedContent, fetchRewards } = this.props;
fetchFeaturedUris();
fetchRewardedContent();
this.continousFetch = setInterval(() => {
fetchFeaturedUris();
fetchRewardedContent();
fetchRewards();
}, 1000 * 60 * 60);
}
componentWillUnmount() {
this.clearContinuousFetch();
}
getCategoryLinkPartByCategory(category: string) {
const channelName = category.substr(category.indexOf('@'));
if (!channelName.includes('#')) {
return null;
}
return channelName;
}
trimClaimIdFromCategory(category: string) {
return category.split('#')[0];
}
continousFetch: ?IntervalID;
clearContinuousFetch() {
if (this.continousFetch) {
clearInterval(this.continousFetch);
this.continousFetch = null;
}
}
render() {
const { featuredUris, fetchingFeaturedUris } = this.props;
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
const failedToLoad = !fetchingFeaturedUris && !hasContent;
return ( return (
<Page notContained isLoading={!hasContent && fetchingFeaturedUris} className="main--no-padding"> <Page className="card">
<FirstRun /> <FileListDiscover
{hasContent && personal
Object.keys(featuredUris).map(category => ( tags={followedTags.map(tag => tag.name)}
<CategoryList injectedItem={<TagsSelect showClose title={__('Make This Your Own')} />}
lazyLoad
key={category}
category={this.trimClaimIdFromCategory(category)}
uris={featuredUris[category]}
categoryLink={this.getCategoryLinkPartByCategory(category)}
/> />
))}
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
</Page> </Page>
); );
} }
}
export default DiscoverPage; export default DiscoverPage;

View file

@ -108,15 +108,15 @@ class FilePage extends React.Component<Props> {
fetchViewCount(claim.claim_id); fetchViewCount(claim.claim_id);
} }
if (prevProps.uri !== uri) {
setViewed(uri);
}
// @if TARGET='app' // @if TARGET='app'
if (fileInfo === undefined) { if (fileInfo === undefined) {
fetchFileInfo(uri); fetchFileInfo(uri);
} }
// @endif // @endif
if (prevProps.uri !== uri) {
setViewed(uri);
}
} }
removeFromSubscriptionNotifications() { removeFromSubscriptionNotifications() {
@ -148,7 +148,8 @@ class FilePage extends React.Component<Props> {
} = this.props; } = this.props;
// File info // File info
const { channel_name: channelName } = claim; const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name;
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage; const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id); const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
const shouldObscureThumbnail = obscureNsfw && nsfw; const shouldObscureThumbnail = obscureNsfw && nsfw;
@ -179,23 +180,12 @@ class FilePage extends React.Component<Props> {
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance; const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
return ( return (
<Page notContained className="main--file-page"> <Page className="main--file-page">
<div className="grid-area--content"> <div className="grid-area--content">
<Button
className="media__uri"
button="alt"
label={uri}
onClick={() => {
clipboard.writeText(uri);
showToast({
message: __('Text copied'),
});
}}
/>
{!fileInfo && insufficientCredits && ( {!fileInfo && insufficientCredits && (
<div className="media__insufficient-credits help--warning"> <div className="media__insufficient-credits help--warning">
{__( {__(
'The publisher has chosen to charge LBC to view this content. Your balance is currently to low to view it.' 'The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it.'
)}{' '} )}{' '}
{__('Checkout')} <Button button="link" navigate="/$/rewards" label={__('the rewards page')} />{' '} {__('Checkout')} <Button button="link" navigate="/$/rewards" label={__('the rewards page')} />{' '}
{__('or send more LBC to your wallet.')} {__('or send more LBC to your wallet.')}
@ -223,12 +213,23 @@ class FilePage extends React.Component<Props> {
<div className="card__media-text">{__("Sorry, looks like we can't preview this file.")}</div> <div className="card__media-text">{__("Sorry, looks like we can't preview this file.")}</div>
</div> </div>
))} ))}
<Button
className="media__uri"
button="alt"
label={uri}
onClick={() => {
clipboard.writeText(uri);
showToast({
message: __('Copied'),
});
}}
/>
</div> </div>
<div className="grid-area--info media__content media__content--large"> <div className="grid-area--info media__content media__content--large">
<h1 className="media__title media__title--large">{title}</h1> <h1 className="media__title media__title--large">{title}</h1>
<div className="media__properties media__properties--large"> <div className="file-properties">
{isRewardContent && ( {isRewardContent && (
<Icon <Icon
size={20} size={20}

View file

@ -1,17 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { selectDownloadedUris, selectIsFetchingFileList } from 'lbry-redux';
selectFileInfosDownloaded,
selectMyClaimsWithoutChannels,
selectIsFetchingFileList,
selectFileListDownloadedSort,
} from 'lbry-redux';
import FileListDownloaded from './view'; import FileListDownloaded from './view';
const select = state => ({ const select = state => ({
fileInfos: selectFileInfosDownloaded(state), downloadedUris: selectDownloadedUris(state),
fetching: selectIsFetchingFileList(state), fetching: selectIsFetchingFileList(state),
claims: selectMyClaimsWithoutChannels(state),
sortBy: selectFileListDownloadedSort(state),
}); });
export default connect( export default connect(

View file

@ -2,26 +2,24 @@
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import Page from 'component/page';
import { PAGES } from 'lbry-redux';
type Props = { type Props = {
fetching: boolean, fetching: boolean,
fileInfos: {}, downloadedUris: Array<string>,
sortBy: string,
}; };
class FileListDownloaded extends React.PureComponent<Props> { function FileListDownloaded(props: Props) {
render() { const { fetching, downloadedUris } = props;
const { fetching, fileInfos, sortBy } = this.props; const hasDownloads = !!downloadedUris.length;
const hasDownloads = fileInfos && Object.values(fileInfos).length > 0;
return ( return (
// Removed the <Page> wapper to try combining this page with UserHistory // Removed the <Page> wapper to try combining this page with UserHistory
// This should eventually move into /components if we want to keep it this way // This should eventually move into /components if we want to keep it this way
<React.Fragment> <React.Fragment>
{hasDownloads ? ( {hasDownloads ? (
<FileList fileInfos={fileInfos} sortBy={sortBy} page={PAGES.DOWNLOADED} /> <div className="card">
<FileList persistedStorageKey="file-list-downloaded" uris={downloadedUris} loading={fetching} />
</div>
) : ( ) : (
<div className="main--empty"> <div className="main--empty">
<section className="card card--section"> <section className="card card--section">
@ -40,6 +38,5 @@ class FileListDownloaded extends React.PureComponent<Props> {
</React.Fragment> </React.Fragment>
); );
} }
}
export default FileListDownloaded; export default FileListDownloaded;

View file

@ -1,12 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectIsFetchingClaimListMine, selectFileListPublishedSort, selectMyClaimsWithoutChannels } from 'lbry-redux'; import { selectIsFetchingClaimListMine, selectMyClaimsWithoutChannels } from 'lbry-redux';
import { doCheckPendingPublishes } from 'redux/actions/publish'; import { doCheckPendingPublishes } from 'redux/actions/publish';
import FileListPublished from './view'; import FileListPublished from './view';
const select = state => ({ const select = state => ({
claims: selectMyClaimsWithoutChannels(state), claims: selectMyClaimsWithoutChannels(state),
fetching: selectIsFetchingClaimListMine(state), fetching: selectIsFetchingClaimListMine(state),
sortBy: selectFileListPublishedSort(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -1,29 +1,32 @@
// @flow // @flow
import React from 'react'; import React, { useEffect } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import Page from 'component/page'; import Page from 'component/page';
import { PAGES } from 'lbry-redux';
type Props = { type Props = {
claims: Array<StreamClaim>, claims: Array<StreamClaim>,
checkPendingPublishes: () => void, checkPendingPublishes: () => void,
fetching: boolean, fetching: boolean,
sortBy: string,
}; };
class FileListPublished extends React.PureComponent<Props> { function FileListPublished(props: Props) {
componentDidMount() { const { checkPendingPublishes, fetching, claims } = props;
const { checkPendingPublishes } = this.props;
checkPendingPublishes(); useEffect(() => {
} checkPendingPublishes();
}, [checkPendingPublishes]);
render() {
const { fetching, claims, sortBy } = this.props;
return ( return (
<Page notContained loading={fetching}> <Page notContained loading={fetching}>
{claims && claims.length ? ( {claims && claims.length ? (
<FileList checkPending fileInfos={claims} sortByHeight sortBy={sortBy} page={PAGES.PUBLISHED} /> <div className="card">
<FileList
persistedStorageKey="file-list-published"
// TODO: adjust selector to only return uris
uris={claims.map(info => `lbry://${info.name}#${info.claim_id}`)}
/>
</div>
) : ( ) : (
<div className="main--empty"> <div className="main--empty">
<section className="card card--section"> <section className="card card--section">
@ -42,6 +45,5 @@ class FileListPublished extends React.PureComponent<Props> {
</Page> </Page>
); );
} }
}
export default FileListPublished; export default FileListPublished;

View file

@ -1,14 +1,34 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doSearch, selectIsSearching } from 'lbry-redux'; import { doSearch, selectIsSearching, makeSelectSearchUris, makeSelectQueryWithOptions, doToast } from 'lbry-redux';
import analytics from 'analytics';
import SearchPage from './view'; import SearchPage from './view';
const select = state => ({ const select = state => ({
isSearching: selectIsSearching(state), isSearching: selectIsSearching(state),
uris: makeSelectSearchUris(makeSelectQueryWithOptions()(state))(state),
}); });
const perform = { const perform = dispatch => ({
doSearch, doSearch: query => doSearch(query),
}; onFeedbackPositive: query => {
analytics.apiSearchFeedback(query, 1);
dispatch(
doToast({
message: __('Thanks for the feedback! You help make the app better for everyone.'),
})
);
},
onFeedbackNegative: query => {
analytics.apiSearchFeedback(query, 0);
dispatch(
doToast({
message: __(
'Thanks for the feedback. Mark has been notified and is currently walking over to his computer to work on this.'
),
})
);
},
});
export default connect( export default connect(
select, select,

View file

@ -1,18 +1,27 @@
// @flow // @flow
import * as ICONS from 'constants/icons';
import React, { useEffect, Fragment } from 'react'; import React, { useEffect, Fragment } from 'react';
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux'; import { isURIValid, normalizeURI } from 'lbry-redux';
import FileTile from 'component/fileTile'; import FileListItem from 'component/fileListItem';
import ChannelTile from 'component/channelTile'; import FileList from 'component/fileList';
import FileListSearch from 'component/fileListSearch';
import Page from 'component/page'; import Page from 'component/page';
import SearchOptions from 'component/searchOptions'; import SearchOptions from 'component/searchOptions';
import Button from 'component/button'; import Button from 'component/button';
type Props = { doSearch: string => void, location: UrlLocation }; type Props = {
doSearch: string => void,
location: UrlLocation,
uris: Array<string>,
onFeedbackNegative: string => void,
onFeedbackPositive: string => void,
};
export default function SearchPage(props: Props) { export default function SearchPage(props: Props) {
const { const {
doSearch, doSearch,
uris,
onFeedbackPositive,
onFeedbackNegative,
location: { search }, location: { search },
} = props; } = props;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
@ -20,10 +29,8 @@ export default function SearchPage(props: Props) {
const isValid = isURIValid(urlQuery); const isValid = isURIValid(urlQuery);
let uri; let uri;
let isChannel;
if (isValid) { if (isValid) {
uri = normalizeURI(urlQuery); uri = normalizeURI(urlQuery);
({ isChannel } = parseURI(uri));
} }
useEffect(() => { useEffect(() => {
@ -42,19 +49,34 @@ export default function SearchPage(props: Props) {
<Button button="alt" navigate={uri} className="media__uri"> <Button button="alt" navigate={uri} className="media__uri">
{uri} {uri}
</Button> </Button>
{isChannel ? ( <FileListItem uri={uri} large />
<ChannelTile size="large" isSearchResult uri={uri} />
) : (
<FileTile size="large" isSearchResult displayHiddenMessage uri={uri} />
)}
</header> </header>
)} )}
<div className="search__results-wrapper"> <div className="card">
<SearchOptions /> <FileList
<FileListSearch query={urlQuery} /> uris={uris}
<div className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div> header={<SearchOptions />}
headerAltControls={
<Fragment>
<span>{__('Find what you were looking for?')}</span>
<Button
button="alt"
description={__('Yes')}
onClick={() => onFeedbackPositive(urlQuery)}
icon={ICONS.YES}
/>
<Button
button="alt"
description={__('No')}
onClick={() => onFeedbackNegative(urlQuery)}
icon={ICONS.NO}
/>
</Fragment>
}
/>
</div> </div>
<div className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div>
</Fragment> </Fragment>
)} )}
</section> </section>

View file

@ -49,7 +49,7 @@ class ShowPage extends React.PureComponent<Props> {
} }
innerContent = ( innerContent = (
<Page notContained> <Page>
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />} {isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
{!isResolvingUri && <span className="empty">{__("There's nothing available at this location.")}</span>} {!isResolvingUri && <span className="empty">{__("There's nothing available at this location.")}</span>}
</Page> </Page>

View file

@ -1,45 +1,25 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { import {
selectSubscriptionClaims, selectSubscriptionClaims,
selectSubscriptions, selectSubscriptions,
selectSubscriptionsBeingFetched, selectSubscriptionsBeingFetched,
selectIsFetchingSubscriptions, selectIsFetchingSubscriptions,
selectUnreadSubscriptions, selectSuggestedChannels,
selectViewMode,
selectFirstRunCompleted,
selectshowSuggestedSubs,
} from 'redux/selectors/subscriptions'; } from 'redux/selectors/subscriptions';
import { import { doFetchMySubscriptions, doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
doFetchMySubscriptions,
doSetViewMode,
doFetchRecommendedSubscriptions,
doCompleteFirstRun,
doShowSuggestedSubs,
} from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import SubscriptionsPage from './view'; import SubscriptionsPage from './view';
const select = state => ({ const select = state => ({
loading: selectIsFetchingSubscriptions(state) || Boolean(Object.keys(selectSubscriptionsBeingFetched(state)).length), loading: selectIsFetchingSubscriptions(state) || Boolean(Object.keys(selectSubscriptionsBeingFetched(state)).length),
subscribedChannels: selectSubscriptions(state), subscribedChannels: selectSubscriptions(state),
autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state), subscriptionContent: selectSubscriptionClaims(state),
allSubscriptions: selectSubscriptionClaims(state), suggestedSubscriptions: selectSuggestedChannels(state),
unreadSubscriptions: selectUnreadSubscriptions(state),
viewMode: selectViewMode(state),
firstRunCompleted: selectFirstRunCompleted(state),
showSuggestedSubs: selectshowSuggestedSubs(state),
}); });
export default connect( export default connect(
select, select,
{ {
doFetchMySubscriptions, doFetchMySubscriptions,
doSetClientSetting,
doSetViewMode,
doFetchRecommendedSubscriptions, doFetchRecommendedSubscriptions,
doCompleteFirstRun,
doShowSuggestedSubs,
} }
)(SubscriptionsPage); )(SubscriptionsPage);

View file

@ -1,51 +0,0 @@
// @flow
import React from 'react';
import Button from 'component/button';
import SuggestedSubscriptions from 'component/subscribeSuggested';
import Yrbl from 'component/yrbl';
type Props = {
showSuggested: boolean,
loadingSuggested: boolean,
numberOfSubscriptions: number,
onFinish: () => void,
doShowSuggestedSubs: () => void,
};
export default function SubscriptionsFirstRun(props: Props) {
const { showSuggested, loadingSuggested, numberOfSubscriptions, doShowSuggestedSubs, onFinish } = props;
return (
<div>
<Yrbl
title={numberOfSubscriptions > 0 ? __('Woohoo!') : __('No subscriptions... yet.')}
subtitle={
<React.Fragment>
<p>
{showSuggested
? __('I hear these channels are pretty good.')
: __("I'll tell you where the good channels are if you find me a wheel.")}
</p>
{!showSuggested && (
<div className="card__actions">
<Button button="primary" label={__('Explore')} onClick={doShowSuggestedSubs} />
</div>
)}
{showSuggested && numberOfSubscriptions > 0 && (
<div className="card__actions">
<Button
button="primary"
onClick={onFinish}
label={`${__('View your')} ${numberOfSubscriptions} ${
numberOfSubscriptions > 1 ? __('subscribed channels') : __('subscribed channel')
}`}
/>
</div>
)}
</React.Fragment>
}
/>
{showSuggested && !loadingSuggested && <SuggestedSubscriptions />}
</div>
);
}

View file

@ -1,124 +0,0 @@
// @flow
import { VIEW_ALL, VIEW_LATEST_FIRST } from 'constants/subscriptions';
import React, { Fragment } from 'react';
import Button from 'component/button';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import FileList from 'component/fileList';
import { FormField } from 'component/common/form';
import FileCard from 'component/fileCard';
import { parseURI } from 'lbry-redux';
import SuggestedSubscriptions from 'component/subscribeSuggested';
import MarkAsRead from 'component/subscribeMarkAsRead';
import Tooltip from 'component/common/tooltip';
import Yrbl from 'component/yrbl';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
type Props = {
viewMode: ViewMode,
doSetViewMode: ViewMode => void,
hasSubscriptions: boolean,
subscriptions: Array<{ uri: string, ...StreamClaim }>,
autoDownload: boolean,
onChangeAutoDownload: (SyntheticInputEvent<*>) => void,
unreadSubscriptions: Array<{ channel: string, uris: Array<string> }>,
};
export default (props: Props) => {
const {
viewMode,
doSetViewMode,
hasSubscriptions,
subscriptions,
autoDownload,
onChangeAutoDownload,
unreadSubscriptions,
} = props;
const index = viewMode === VIEW_ALL ? 0 : 1;
const onTabChange = index => (index === 0 ? doSetViewMode(VIEW_ALL) : doSetViewMode(VIEW_LATEST_FIRST));
return (
<Fragment>
{hasSubscriptions && (
<Tabs onChange={onTabChange} index={index}>
<TabList className="main__item--extend-outside">
<Tab>{__('All Subscriptions')}</Tab>
<Tab>{__('Latest Only')}</Tab>
<Tooltip onComponent body={__('Automatically download new subscriptions.')}>
<FormField
type="setting"
name="auto_download"
onChange={onChangeAutoDownload}
checked={autoDownload}
label={__('Auto download')}
labelOnLeft
/>
</Tooltip>
</TabList>
<TabPanels
header={
<HiddenNsfwClaims
uris={subscriptions.reduce((arr, { name, claim_id: claimId }) => {
if (name && claimId) {
arr.push(`lbry://${name}#${claimId}`);
}
return arr;
}, [])}
/>
}
>
<TabPanel>
<div className="card__title card__title--flex">
<span>{__('Your subscriptions')}</span>
{unreadSubscriptions.length > 0 && <MarkAsRead />}
</div>
<FileList hideFilter sortByHeight fileInfos={subscriptions} />
</TabPanel>
<TabPanel>
{unreadSubscriptions.length ? (
unreadSubscriptions.map(({ channel, uris }) => {
const { claimName } = parseURI(channel);
return (
<section key={channel}>
<h2 className="card__title card__title--flex">
<Button button="link" navigate={channel} label={claimName} />
<MarkAsRead channel={channel} />
</h2>
<section className="media-group--list">
<ul className="card__list">
{uris.map(uri => (
<FileCard key={uri} uri={uri} />
))}
</ul>
</section>
</section>
);
})
) : (
<Fragment>
<Yrbl title={__('All caught up!')} subtitle={__('You might like the channels below.')} />
<SuggestedSubscriptions />
</Fragment>
)}
</TabPanel>
</TabPanels>
</Tabs>
)}
{!hasSubscriptions && (
<Fragment>
<Yrbl
type="sad"
title={__('Oh no! What happened to your subscriptions?')}
subtitle={__('These channels look pretty cool.')}
/>
<SuggestedSubscriptions />
</Fragment>
)}
</Fragment>
);
};

View file

@ -1,105 +1,55 @@
// @flow // @flow
import * as SETTINGS from 'constants/settings'; import React, { useEffect, useState } from 'react';
import React, { PureComponent } from 'react';
import Page from 'component/page'; import Page from 'component/page';
import FirstRun from './internal/first-run'; import FileList from 'component/fileList';
import UserSubscriptions from './internal/user-subscriptions'; import Button from 'component/button';
type Props = { type Props = {
subscribedChannels: Array<string>, // The channels a user is subscribed to subscribedChannels: Array<string>, // The channels a user is subscribed to
unreadSubscriptions: Array<{ subscriptionContent: Array<{ uri: string, ...StreamClaim }>,
channel: string, suggestedSubscriptions: Array<{ uri: string }>,
uris: Array<string>,
}>,
allSubscriptions: Array<{ uri: string, ...StreamClaim }>,
loading: boolean, loading: boolean,
autoDownload: boolean,
viewMode: ViewMode,
doSetViewMode: ViewMode => void,
doFetchMySubscriptions: () => void, doFetchMySubscriptions: () => void,
doSetClientSetting: (string, boolean) => void,
doFetchRecommendedSubscriptions: () => void, doFetchRecommendedSubscriptions: () => void,
loadingSuggested: boolean,
firstRunCompleted: boolean,
doCompleteFirstRun: () => void,
doShowSuggestedSubs: () => void,
showSuggestedSubs: boolean,
}; };
export default class SubscriptionsPage extends PureComponent<Props> { export default function SubscriptionsPage(props: Props) {
constructor() {
super();
(this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this);
}
componentDidMount() {
const { const {
subscriptionContent,
subscribedChannels,
doFetchMySubscriptions, doFetchMySubscriptions,
doFetchRecommendedSubscriptions, doFetchRecommendedSubscriptions,
allSubscriptions, suggestedSubscriptions,
firstRunCompleted, loading,
doShowSuggestedSubs, } = props;
} = this.props; const hasSubscriptions = !!subscribedChannels.length;
const [showSuggested, setShowSuggested] = useState(!hasSubscriptions);
useEffect(() => {
doFetchMySubscriptions(); doFetchMySubscriptions();
doFetchRecommendedSubscriptions(); doFetchRecommendedSubscriptions();
}, [doFetchMySubscriptions, doFetchRecommendedSubscriptions]);
// For channels that already have subscriptions, show the suggested subs right away
// This can probably be removed at a future date, it is just to make it so the "view your x subscriptions" button shows up right away
// Existing users will still go through the "first run"
if (!firstRunCompleted && allSubscriptions.length) {
doShowSuggestedSubs();
}
}
onAutoDownloadChange(event: SyntheticInputEvent<*>) {
this.props.doSetClientSetting(SETTINGS.AUTO_DOWNLOAD, event.target.checked);
}
render() {
const {
subscribedChannels,
allSubscriptions,
loading,
autoDownload,
viewMode,
doSetViewMode,
loadingSuggested,
firstRunCompleted,
doCompleteFirstRun,
doShowSuggestedSubs,
showSuggestedSubs,
unreadSubscriptions,
} = this.props;
const numberOfSubscriptions = subscribedChannels && subscribedChannels.length;
return ( return (
// Only pass in the loading prop if there are no subscriptions <Page>
// If there are any, let the page update in the background <div className="card">
// The loading prop removes children and shows a loading spinner <FileList
<Page notContained loading={loading && !subscribedChannels} className="main--no-padding-top"> loading={loading}
{firstRunCompleted ? ( header={<h1>{showSuggested ? __('Discover New Channels') : __('Latest From Your Subscriptions')}</h1>}
<UserSubscriptions headerAltControls={
viewMode={viewMode} <Button
doSetViewMode={doSetViewMode} button="alt"
hasSubscriptions={numberOfSubscriptions > 0} label={showSuggested ? hasSubscriptions && __('View Your Subscriptions') : __('Find New Channels')}
subscriptions={allSubscriptions} onClick={() => setShowSuggested(!showSuggested)}
autoDownload={autoDownload}
onChangeAutoDownload={this.onAutoDownloadChange}
unreadSubscriptions={unreadSubscriptions}
loadingSuggested={loadingSuggested}
/> />
) : ( }
<FirstRun uris={
showSuggested={showSuggestedSubs} showSuggested
doShowSuggestedSubs={doShowSuggestedSubs} ? suggestedSubscriptions.map(sub => sub.uri)
loadingSuggested={loadingSuggested} : subscriptionContent.map(sub => sub.permanent_url)
numberOfSubscriptions={numberOfSubscriptions} }
onFinish={doCompleteFirstRun}
/> />
)} </div>
</Page> </Page>
); );
} }
}

14
src/ui/page/tags/index.js Normal file
View file

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import Tags from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
});
const perform = {};
export default connect(
select,
perform
)(Tags);

28
src/ui/page/tags/view.jsx Normal file
View file

@ -0,0 +1,28 @@
// @flow
import React from 'react';
import Page from 'component/page';
import FileListDiscover from 'component/fileListDiscover';
type Props = {
location: { search: string },
};
function TagsPage(props: Props) {
const {
location: { search },
} = props;
const urlParams = new URLSearchParams(search);
const tagsQuery = urlParams.get('t') || '';
const tags = tagsQuery.split(',');
return (
<Page>
<div className="card">
<FileListDiscover tags={tags} />
</div>
</Page>
);
}
export default TagsPage;

View file

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import TagsEdit from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
});
const perform = {};
export default connect(
select,
perform
)(TagsEdit);

View file

@ -0,0 +1,18 @@
// @flow
import React from 'react';
import Page from 'component/page';
import TagsSelect from 'component/tagsSelect';
type Props = {};
function DiscoverPage(props: Props) {
return (
<Page>
<div className="card">
<TagsSelect showClose={false} title={__('Find New Tags To Follow')} />
</div>
</Page>
);
}
export default DiscoverPage;

View file

@ -1,6 +1,13 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router'; import { connectRouter } from 'connected-react-router';
import { claimsReducer, fileInfoReducer, searchReducer, walletReducer, notificationsReducer } from 'lbry-redux'; import {
claimsReducer,
fileInfoReducer,
searchReducer,
walletReducer,
notificationsReducer,
tagsReducer,
} from 'lbry-redux';
import { userReducer, rewardsReducer, costInfoReducer, blacklistReducer, homepageReducer, statsReducer } from 'lbryinc'; import { userReducer, rewardsReducer, costInfoReducer, blacklistReducer, homepageReducer, statsReducer } from 'lbryinc';
import appReducer from 'redux/reducers/app'; import appReducer from 'redux/reducers/app';
import availabilityReducer from 'redux/reducers/availability'; import availabilityReducer from 'redux/reducers/availability';
@ -27,6 +34,7 @@ export default history =>
settings: settingsReducer, settings: settingsReducer,
stats: statsReducer, stats: statsReducer,
subscriptions: subscriptionsReducer, subscriptions: subscriptionsReducer,
tags: tagsReducer,
user: userReducer, user: userReducer,
wallet: walletReducer, wallet: walletReducer,
}); });

View file

@ -391,12 +391,6 @@ export function doConditionalAuthNavigate(newSession) {
}; };
} }
export function doToggleEnhancedLayout() {
return {
type: ACTIONS.ENNNHHHAAANNNCEEE,
};
}
export function doToggleSearchExpanded() { export function doToggleSearchExpanded() {
return { return {
type: ACTIONS.TOGGLE_SEARCH_EXPANDED, type: ACTIONS.TOGGLE_SEARCH_EXPANDED,

View file

@ -21,7 +21,6 @@ import {
selectBalance, selectBalance,
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
parseURI, parseURI,
creditsToString,
doError, doError,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc'; import { makeSelectCostInfoForUri } from 'lbryinc';
@ -293,13 +292,7 @@ export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize:
data: { uri, page }, data: { uri, page },
}); });
const { claimName, claimId } = parseURI(uri); Lbry.claim_search({ channel: uri, is_controlling: true, page, page_size: pageSize }).then(result => {
let channelName = claimName;
if (claimId) {
channelName += `#${claimId}`;
}
Lbry.claim_search({ channel_name: channelName, page, page_size: pageSize }).then(result => {
const { items: claimsInChannel, page: returnedPage } = result; const { items: claimsInChannel, page: returnedPage } = result;
if (claimsInChannel && claimsInChannel.length) { if (claimsInChannel && claimsInChannel.length) {

View file

@ -7,7 +7,7 @@ import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions'; import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux'; import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content'; import { doPurchaseUri } from 'redux/actions/content';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000; const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
@ -35,8 +35,7 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
Lbryio.call('subscription', 'list') Lbryio.call('subscription', 'list')
.then(dbSubscriptions => { .then(dbSubscriptions => {
const storedSubscriptions = dbSubscriptions || []; const storedSubscriptions = dbSubscriptions || [];
// // User has no subscriptions in db or redux
// User has no subscriptions in db or redux
if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) { if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
return []; return [];
} }
@ -45,25 +44,12 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
// If something is in the db, but not in redux, add it to redux // If something is in the db, but not in redux, add it to redux
// If something is in redux, but not in the db, add it to the db // If something is in redux, but not in the db, add it to the db
if (storedSubscriptions.length !== reduxSubscriptions.length) { if (storedSubscriptions.length !== reduxSubscriptions.length) {
const dbSubMap = {};
const reduxSubMap = {}; const reduxSubMap = {};
const subsNotInDB = [];
const subscriptionsToReturn = reduxSubscriptions.slice(); const subscriptionsToReturn = reduxSubscriptions.slice();
storedSubscriptions.forEach(sub => {
dbSubMap[sub.claim_id] = 1;
});
reduxSubscriptions.forEach(sub => { reduxSubscriptions.forEach(sub => {
const { claimId } = parseURI(sub.uri); const { claimId } = parseURI(sub.uri);
reduxSubMap[claimId] = 1; reduxSubMap[claimId] = 1;
if (!dbSubMap[claimId]) {
subsNotInDB.push({
claim_id: claimId,
channel_name: sub.channelName,
});
}
}); });
storedSubscriptions.forEach(sub => { storedSubscriptions.forEach(sub => {
@ -73,13 +59,7 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
} }
}); });
return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload))) return subscriptionsToReturn;
.then(() => subscriptionsToReturn)
.catch(
() =>
// let it fail, we will try again when the navigate to the subscriptions page
subscriptionsToReturn
);
} }
// DB is already synced, just return the subscriptions in redux // DB is already synced, just return the subscriptions in redux
@ -223,10 +203,9 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`); throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
} }
const { claimId } = parseURI(subscriptionUri);
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel? // We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
Lbry.claim_search({ channel_id: claimId, page: 1, page_size: PAGE_SIZE }).then(claimListByChannel => { Lbry.claim_search({ channel: subscriptionUri, is_controlling: true, page: 1, page_size: PAGE_SIZE }).then(
claimListByChannel => {
const { items: claimsInChannel } = claimListByChannel; const { items: claimsInChannel } = claimListByChannel;
// may happen if subscribed to an abandoned channel or an empty channel // may happen if subscribed to an abandoned channel or an empty channel
@ -301,7 +280,8 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
page: 1, page: 1,
}, },
}); });
}); }
);
}; };
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => { export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
@ -394,13 +374,3 @@ export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => {
}) })
); );
}; };
export const doCompleteFirstRun = () => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
});
export const doShowSuggestedSubs = () => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
});

View file

@ -2,14 +2,7 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
// @if TARGET='app'
// $FlowFixMe
import { remote } from 'electron'; import { remote } from 'electron';
// @endif
// @if TARGET='web'
// $FlowFixMe
import { remote } from 'web/stubs';
// @endif
// @if TARGET='app' // @if TARGET='app'
const win = remote.BrowserWindow.getFocusedWindow(); const win = remote.BrowserWindow.getFocusedWindow();
@ -43,7 +36,6 @@ export type AppState = {
isUpgradeAvailable: ?boolean, isUpgradeAvailable: ?boolean,
isUpgradeSkipped: ?boolean, isUpgradeSkipped: ?boolean,
hasClickedComment: boolean, hasClickedComment: boolean,
enhancedLayout: boolean,
searchOptionsExpanded: boolean, searchOptionsExpanded: boolean,
}; };
@ -228,11 +220,6 @@ reducers[ACTIONS.AUTHENTICATION_FAILURE] = state =>
modal: MODALS.AUTHENTICATION_FAILURE, modal: MODALS.AUTHENTICATION_FAILURE,
}); });
reducers[ACTIONS.ENNNHHHAAANNNCEEE] = state =>
Object.assign({}, state, {
enhancedLayout: !state.enhancedLayout,
});
reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state => reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state =>
Object.assign({}, state, { Object.assign({}, state, {
searchOptionsExpanded: !state.searchOptionsExpanded, searchOptionsExpanded: !state.searchOptionsExpanded,

View file

@ -19,11 +19,9 @@ const defaultState = {
[SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true), [SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
[SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false), [SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false),
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, false), [SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, false),
[SETTINGS.INVITE_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.INVITE_ACKNOWLEDGED, false),
[SETTINGS.FIRST_RUN_COMPLETED]: getLocalStorageSetting(SETTINGS.FIRST_RUN_COMPLETED, false),
[SETTINGS.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run [SETTINGS.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'), [SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'dark'), [SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'light'),
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []), [SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false), [SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false), [SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),

View file

@ -134,14 +134,6 @@ export default handleActions(
...state, ...state,
loadingSuggested: false, loadingSuggested: false,
}), }),
[ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED]: (state: SubscriptionState): SubscriptionState => ({
...state,
firstRunCompleted: true,
}),
[ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
...state,
showSuggestedSubs: true,
}),
}, },
defaultState defaultState
); );

View file

@ -1,6 +1,4 @@
import * as SETTINGS from 'constants/settings';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { makeSelectClientSetting } from 'redux/selectors/settings';
export const selectState = state => state.app || {}; export const selectState = state => state.app || {};
@ -121,20 +119,7 @@ export const selectModal = createSelector(
} }
); );
export const selectEnhancedLayout = createSelector(
selectState,
state => state.enhancedLayout
);
export const selectSearchOptionsExpanded = createSelector( export const selectSearchOptionsExpanded = createSelector(
selectState, selectState,
state => state.searchOptionsExpanded state => state.searchOptionsExpanded
); );
export const selectShouldShowInviteGuide = createSelector(
makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED),
makeSelectClientSetting(SETTINGS.INVITE_ACKNOWLEDGED),
(firstRunCompleted, inviteAcknowledged) => {
return firstRunCompleted ? !inviteAcknowledged : false;
}
);

View file

@ -9,7 +9,6 @@ import {
parseURI, parseURI,
} from 'lbry-redux'; } from 'lbry-redux';
import { swapKeyAndValue } from 'util/swap-json'; import { swapKeyAndValue } from 'util/swap-json';
import { shuffleArray } from 'util/shuffleArray';
// Returns the entire subscriptions state // Returns the entire subscriptions state
const selectState = state => state.subscriptions || {}; const selectState = state => state.subscriptions || {};
@ -86,13 +85,10 @@ export const selectSuggestedChannels = createSelector(
} }
}); });
return Object.keys(suggestedChannels) return Object.keys(suggestedChannels).map(uri => ({
.map(uri => ({
uri, uri,
label: suggestedChannels[uri], label: suggestedChannels[uri],
})) }));
.sort(shuffleArray)
.slice(0, 5);
} }
); );

View file

@ -10,7 +10,6 @@
@import 'init/gui'; @import 'init/gui';
@import 'component/animation'; @import 'component/animation';
@import 'component/badge'; @import 'component/badge';
@import 'component/banner';
@import 'component/button'; @import 'component/button';
@import 'component/card'; @import 'component/card';
@import 'component/channel'; @import 'component/channel';
@ -19,6 +18,8 @@
@import 'component/dat-gui'; @import 'component/dat-gui';
@import 'component/expandable'; @import 'component/expandable';
@import 'component/file-download'; @import 'component/file-download';
@import 'component/file-list';
@import 'component/file-properties';
@import 'component/file-render'; @import 'component/file-render';
@import 'component/form-field'; @import 'component/form-field';
@import 'component/header'; @import 'component/header';
@ -33,7 +34,6 @@
@import 'component/notice'; @import 'component/notice';
@import 'component/pagination'; @import 'component/pagination';
@import 'component/placeholder'; @import 'component/placeholder';
@import 'component/scrollbar';
@import 'component/search'; @import 'component/search';
@import 'component/snack-bar'; @import 'component/snack-bar';
@import 'component/spinner'; @import 'component/spinner';
@ -42,6 +42,7 @@
@import 'component/syntax-highlighter'; @import 'component/syntax-highlighter';
@import 'component/table'; @import 'component/table';
@import 'component/tabs'; @import 'component/tabs';
@import 'component/tags';
@import 'component/time'; @import 'component/time';
@import 'component/toggle'; @import 'component/toggle';
@import 'component/tooltip'; @import 'component/tooltip';

View file

@ -1 +1,18 @@
@import '~@lbry/components/sass/badge/_index.scss'; @import '~@lbry/components/sass/badge/_index.scss';
.badge--tag {
@extend .badge;
background-color: lighten($lbry-teal-5, 55%);
color: darken($lbry-teal-5, 20%);
[data-mode='dark'] & {
color: lighten($lbry-teal-5, 60%);
background-color: rgba($lbry-teal-5, 0.3);
}
}
.badge--alert {
@extend .badge;
background-color: $lbry-red-2;
color: $lbry-white;
}

View file

@ -1,31 +0,0 @@
.banner {
display: flex;
overflow: hidden;
background-color: $lbry-black;
color: $lbry-white;
}
.banner--first-run {
height: 310px;
padding-right: var(--spacing-vertical-medium);
// Adjust this class inside other `.banner--xxx` styles for control over animation
.banner__item--static-for-animation {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.banner__item {
&:not(:first-child) {
margin-left: var(--spacing-vertical-large);
}
}
.banner__content {
display: flex;
align-items: center;
height: 100%;
}

View file

@ -3,16 +3,8 @@
.button { .button {
display: inline-block; display: inline-block;
.button__content {
display: flex;
align-items: center;
height: 100%;
}
svg { svg {
stroke-width: 1.9; stroke-width: 1.9;
width: 1.2rem;
height: 1.2rem;
position: relative; position: relative;
color: $lbry-gray-5; color: $lbry-gray-5;
@ -23,12 +15,6 @@
height: 1.4rem; height: 1.4rem;
} }
} }
// Handle icons on the left or right side of the button label
svg + .button__label,
.button__label + svg {
margin-left: var(--spacing-vertical-miniscule);
}
} }
.button--primary { .button--primary {
@ -56,16 +42,57 @@
box-sizing: border-box; box-sizing: border-box;
} }
.button--alt {
padding: 0;
}
.button--uri-indicator { .button--uri-indicator {
max-width: 100%; max-width: 100%;
height: 1.2em; height: 1.2em;
vertical-align: text-top; vertical-align: text-top;
overflow: hidden;
text-align: left; text-align: left;
text-overflow: ellipsis; text-overflow: ellipsis;
transition: color 0.2s; transition: color 0.2s;
&:hover { &:hover {
color: $lbry-teal-3; color: $lbry-teal-5;
} }
} }
.button--close {
position: absolute;
top: var(--spacing-miniscule);
right: var(--spacing-miniscule);
padding: 0.3rem;
transition: all var(--transition-duration) var(--transition-style);
&:hover {
background-color: $lbry-red-3;
color: $lbry-white;
border-radius: var(--card-radius);
}
}
.button--subscribe {
vertical-align: text-top;
align-items: flex-start;
}
.button__label {
// white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// display: flex;
// align-items: center;
}
// Handle icons on the left or right side of the button label
svg + .button__label,
.button__label + svg {
margin-left: var(--spacing-miniscule);
}
.button__content {
display: flex;
align-items: center;
}

View file

@ -1,12 +1,13 @@
.card { .card {
background-color: $lbry-white; background-color: $lbry-white;
margin-bottom: var(--spacing-vertical-xlarge); margin-bottom: var(--spacing-xlarge);
position: relative; position: relative;
border-radius: var(--card-radius); border-radius: var(--card-radius);
box-shadow: var(--card-box-shadow) $lbry-gray-1; box-shadow: var(--card-box-shadow) $lbry-gray-1;
overflow: hidden;
html[data-mode='dark'] & { html[data-mode='dark'] & {
background-color: rgba($lbry-white, 0.1); background-color: lighten($lbry-black, 5%);
box-shadow: var(--card-box-shadow) darken($lbry-gray-1, 80%); box-shadow: var(--card-box-shadow) darken($lbry-gray-1, 80%);
} }
} }
@ -21,10 +22,11 @@
} }
.card--section { .card--section {
padding: var(--spacing-vertical-large); position: relative;
padding: var(--spacing-large);
.card__content:not(:last-of-type) { .card__content:not(:last-of-type) {
margin-bottom: var(--spacing-vertical-large); margin-bottom: var(--spacing-large);
} }
} }
@ -39,6 +41,10 @@
justify-content: space-between; justify-content: space-between;
} }
.card--modal {
box-shadow: none;
}
// C A R D // C A R D
// A C T I O N S // A C T I O N S
@ -47,7 +53,7 @@
font-size: 1.15rem; font-size: 1.15rem;
> *:not(:last-child) { > *:not(:last-child) {
margin-right: var(--spacing-vertical-medium); margin-right: var(--spacing-medium);
} }
} }
@ -74,7 +80,7 @@
} }
.card__actions--top-space { .card__actions--top-space {
padding-top: var(--spacing-vertical-small); padding-top: var(--spacing-small);
} }
// C A R D // C A R D
@ -84,13 +90,12 @@
font-size: 1.25rem; font-size: 1.25rem;
p:not(:last-child) { p:not(:last-child) {
margin-bottom: var(--spacing-vertical-medium); margin-bottom: var(--spacing-medium);
}
} }
.badge { .card__content--large {
bottom: -0.15rem; font-size: 4rem;
position: relative;
}
} }
// C A R D // C A R D
@ -100,25 +105,17 @@
position: relative; position: relative;
&:not(.card__header--flat) { &:not(.card__header--flat) {
margin-bottom: var(--spacing-vertical-medium); margin-bottom: var(--spacing-medium);
} }
} }
// C A R D
// I N T E R N A L
.card__internal-links {
top: 2rem;
right: 2rem;
position: absolute;
}
// C A R D // C A R D
// L I S T // L I S T
.card__list { .card__list {
display: grid; display: grid;
grid-gap: var(--spacing-vertical-medium); grid-gap: var(--spacing-medium);
margin-top: var(--spacing-large);
// Depending on screen width, the amount of items in // Depending on screen width, the amount of items in
// each row change and are auto-sized // each row change and are auto-sized
@ -135,31 +132,21 @@
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 7), 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(100% / 7), 1fr));
} }
@media (min-width: 1051px) and (max-width: 1550px) { @media (min-width: 1200px) and (max-width: 1550px) {
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 6), 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(100% / 6), 1fr));
} }
@media (min-width: 901px) and (max-width: 1050px) {
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr));
} }
@media (min-width: 751px) and (max-width: 900px) {
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 4), 1fr));
}
@media (max-width: 750px) {
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 3), 1fr));
}
}
.card__list--rewards { .card__list--rewards {
column-count: 2; column-count: 2;
column-gap: var(--spacing-vertical-medium); column-gap: var(--spacing-medium);
margin-bottom: var(--spacing-vertical-large); margin-bottom: var(--spacing-large);
.card { .card {
display: inline-block; display: inline-block;
margin: 0 0 var(--spacing-vertical-medium); margin: 0 0 var(--spacing-medium);
width: 100%; width: 100%;
} }
} }
@ -169,8 +156,7 @@
.card__message { .card__message {
border-left: 0.5rem solid; border-left: 0.5rem solid;
padding: var(--spacing-vertical-medium) var(--spacing-vertical-medium) var(--spacing-vertical-medium) padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-medium) var(--spacing-large);
var(--spacing-vertical-large);
&:not(&--error):not(&--failure):not(&--success) { &:not(&--error):not(&--failure):not(&--success) {
background-color: rgba($lbry-teal-1, 0.1); background-color: rgba($lbry-teal-1, 0.1);
@ -198,22 +184,24 @@
.card__subtitle { .card__subtitle {
@extend .help; @extend .help;
background-color: lighten($lbry-gray-1, 7%); color: darken($lbry-gray-5, 25%);
color: darken($lbry-gray-5, 30%);
font-size: 1.15rem; font-size: 1.15rem;
margin-bottom: var(--spacing-vertical-small); margin-bottom: var(--spacing-small);
flex: 1;
p { p {
margin-bottom: var(--spacing-vertical-small); margin-bottom: var(--spacing-small);
} }
.badge { .badge {
bottom: -0.12rem; bottom: -0.12rem;
position: relative; position: relative;
margin-left: 0;
} }
[data-mode='dark'] & { [data-mode='dark'] & {
background-color: darken($lbry-gray-5, 20%); // TODO: dark
// background-color: darken($lbry-gray-5, 20%);
} }
} }
@ -223,10 +211,10 @@
.card__title { .card__title {
font-size: 2rem; font-size: 2rem;
font-weight: 600; font-weight: 600;
margin-bottom: var(--spacing-vertical-medium); margin-bottom: var(--spacing-medium);
+ .card__content { + .card__content {
margin-top: var(--spacing-vertical-medium); margin-top: var(--spacing-medium);
} }
} }
@ -235,7 +223,7 @@
align-items: center; align-items: center;
& > *:not(:last-child) { & > *:not(:last-child) {
margin-right: var(--spacing-vertical-medium); margin-right: var(--spacing-medium);
} }
} }

View file

@ -7,6 +7,8 @@ $metadata-z-index: 1;
align-items: flex-end; align-items: flex-end;
box-sizing: content-box; box-sizing: content-box;
color: $lbry-white; color: $lbry-white;
border-top-left-radius: var(--card-radius);
border-top-right-radius: var(--card-radius);
} }
.channel-cover__custom { .channel-cover__custom {
@ -24,13 +26,19 @@ $metadata-z-index: 1;
} }
.channel-thumbnail { .channel-thumbnail {
position: absolute;
display: flex; display: flex;
left: var(--spacing-main-padding); height: 5.3rem;
height: var(--channel-thumbnail-size); width: 5.4rem;
width: var(--channel-thumbnail-size);
background-size: cover; background-size: cover;
margin-right: var(--spacing-medium);
}
.channel__thumbnail--channel-page {
position: absolute;
height: var(--channel-thumbnail-width);
width: var(--channel-thumbnail-width);
box-shadow: 0px 8px 40px -3px $lbry-black; box-shadow: 0px 8px 40px -3px $lbry-black;
left: var(--spacing-medium);
} }
.channel-thumbnail__custom { .channel-thumbnail__custom {
@ -44,7 +52,7 @@ $metadata-z-index: 1;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
align-self: flex-end; align-self: flex-end;
margin-bottom: -1px; // margin-bottom: -1px;
} }
.channel-thumbnail, .channel-thumbnail,
@ -70,14 +78,15 @@ $metadata-z-index: 1;
z-index: $metadata-z-index; z-index: $metadata-z-index;
// Jump over the thumbnail photo because it is absolutely positioned // Jump over the thumbnail photo because it is absolutely positioned
// Then add normal page spacing, _then_ add the actual padding // Then add normal page spacing, _then_ add the actual padding
margin-left: calc(var(--channel-thumbnail-size) + var(--spacing-main-padding)); padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-large));
padding-left: var(--spacing-vertical-large); // padding-left: var(--spacing-large);
padding-bottom: var(--spacing-vertical-medium); padding-bottom: var(--spacing-medium);
} }
.channel__title { .channel__title {
font-size: 3rem; font-size: 3rem;
font-weight: 800; font-weight: 800;
margin-right: var(--spacing-large);
} }
.channel__url { .channel__url {
@ -85,3 +94,8 @@ $metadata-z-index: 1;
margin-top: -0.25rem; margin-top: -0.25rem;
color: rgba($lbry-white, 0.75); color: rgba($lbry-white, 0.75);
} }
// TODO: rename
.channel__data {
min-height: 10rem;
}

View file

@ -78,7 +78,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
padding: 0 var(--spacing-vertical-large); padding: 0 var(--spacing-large);
background-color: #000; background-color: #000;
} }

Some files were not shown because too many files have changed in this diff Show more