tags
This commit is contained in:
parent
be89a11904
commit
60543562aa
140 changed files with 2059 additions and 3305 deletions
|
@ -60,7 +60,7 @@
|
|||
"@exponent/electron-cookies": "^2.0.0",
|
||||
"@hot-loader/react-dom": "16.8",
|
||||
"@lbry/color": "^1.0.2",
|
||||
"@lbry/components": "^2.7.0",
|
||||
"@lbry/components": "^2.7.2",
|
||||
"@reach/rect": "^0.2.1",
|
||||
"@reach/tabs": "^0.1.5",
|
||||
"@types/three": "^0.93.1",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"jsmediatags": "^3.8.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"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",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
@ -153,6 +153,7 @@
|
|||
"react-router": "^5.0.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-simplemde-editor": "^4.0.0",
|
||||
"react-spring": "^8.0.20",
|
||||
"react-toggle": "^4.0.2",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^4.8.0",
|
||||
|
@ -191,7 +192,7 @@
|
|||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.37.4",
|
||||
"lbrynetDaemonVersion": "0.38.0rc6",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet"
|
||||
|
|
|
@ -84,19 +84,6 @@ export default appState => {
|
|||
window.loadURL(rendererURL + deepLinkingURI);
|
||||
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 => {
|
||||
if (!appState.isQuitting && !appState.autoUpdateAccepted) {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
export const clipboard = () => {
|
||||
throw 'Fix me!';
|
||||
throw new Error('Fix me!');
|
||||
};
|
||||
|
||||
export const ipcRenderer = () => {
|
||||
throw 'Fix me!';
|
||||
};
|
||||
|
||||
export const remote = () => {
|
||||
throw 'Fix me!';
|
||||
throw new Error('Fix me!');
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const callable = () => {
|
||||
throw Error('Need to fix this stub');
|
||||
};
|
||||
const returningCallable = value => () => value;
|
||||
|
||||
export const remote = {
|
||||
dialog: {
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
import { hot } from 'react-hot-loader/root';
|
||||
import { connect } from 'react-redux';
|
||||
import { doUpdateBlockHeight, doError } from 'lbry-redux';
|
||||
import { doToggleEnhancedLayout } from 'redux/actions/app';
|
||||
import { selectUser } from 'lbryinc';
|
||||
import { selectUser, doRewardList, doFetchRewardedContent } from 'lbryinc';
|
||||
import { selectThemePath } from 'redux/selectors/settings';
|
||||
import { selectEnhancedLayout } from 'redux/selectors/app';
|
||||
import App from './view';
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
theme: selectThemePath(state),
|
||||
enhancedLayout: selectEnhancedLayout(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
alertError: errorList => dispatch(doError(errorList)),
|
||||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||
toggleEnhancedLayout: () => dispatch(doToggleEnhancedLayout()),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
});
|
||||
|
||||
export default hot(
|
||||
|
|
|
@ -1,82 +1,54 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Router from 'component/router/index';
|
||||
import ModalRouter from 'modal/modalRouter';
|
||||
import ReactModal from 'react-modal';
|
||||
import SideBar from 'component/sideBar';
|
||||
import Header from 'component/header';
|
||||
import { openContextMenu } from 'util/context-menu';
|
||||
import EnhancedLayoutListener from 'util/enhanced-layout';
|
||||
import useKonamiListener from 'util/enhanced-layout';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
||||
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;
|
||||
|
||||
type Props = {
|
||||
alertError: (string | {}) => void,
|
||||
pageTitle: ?string,
|
||||
language: string,
|
||||
theme: string,
|
||||
updateBlockHeight: () => void,
|
||||
toggleEnhancedLayout: () => void,
|
||||
enhancedLayout: boolean,
|
||||
fetchRewards: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
};
|
||||
|
||||
class App extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
const { updateBlockHeight, toggleEnhancedLayout, alertError, theme } = this.props;
|
||||
function App(props: Props) {
|
||||
const { theme, fetchRewards, fetchRewardedContent } = props;
|
||||
const appRef = useRef();
|
||||
const isEnhancedLayout = useKonamiListener();
|
||||
|
||||
// TODO: create type for this object
|
||||
// it lives in jsonrpc.js
|
||||
document.addEventListener('unhandledError', (event: any) => {
|
||||
alertError(event.detail);
|
||||
});
|
||||
useEffect(() => {
|
||||
ReactModal.setAppElement(appRef.current);
|
||||
fetchRewards();
|
||||
fetchRewardedContent();
|
||||
}, [fetchRewards, fetchRewardedContent]);
|
||||
|
||||
useEffect(() => {
|
||||
// $FlowFixMe
|
||||
document.documentElement.setAttribute('data-mode', theme);
|
||||
}, [theme]);
|
||||
|
||||
ReactModal.setAppElement('#window'); // fuck this
|
||||
return (
|
||||
<div ref={appRef} onContextMenu={e => openContextMenu(e)}>
|
||||
<Header />
|
||||
|
||||
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 (
|
||||
<div id="window" onContextMenu={e => openContextMenu(e)}>
|
||||
<Header />
|
||||
<SideBar />
|
||||
|
||||
<div className="main-wrapper">
|
||||
<div className="main-wrapper">
|
||||
<div className="main-wrapper-inner">
|
||||
<Router />
|
||||
<SideBar />
|
||||
</div>
|
||||
|
||||
<ModalRouter />
|
||||
{enhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<ModalRouter />
|
||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -62,8 +62,8 @@ class Button extends React.PureComponent<Props> {
|
|||
'button--primary': button === 'primary',
|
||||
'button--secondary': button === 'secondary',
|
||||
'button--alt': button === 'alt',
|
||||
'button--danger': button === 'danger',
|
||||
'button--inverse': button === 'inverse',
|
||||
'button--close': button === 'close',
|
||||
'button--disabled': disabled,
|
||||
'button--link': button === 'link',
|
||||
'button--constrict': constrict,
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -22,8 +22,8 @@ function ChannelContent(props: Props) {
|
|||
const showAbout = description || email || website;
|
||||
|
||||
return (
|
||||
<section>
|
||||
{!showAbout && <h2 className="empty">{__('Nothing here yet')}</h2>}
|
||||
<section className="card--section">
|
||||
{!showAbout && <h2 className="main--empty empty">{__('Nothing here yet')}</h2>}
|
||||
{showAbout && (
|
||||
<Fragment>
|
||||
{description && (
|
||||
|
|
|
@ -19,7 +19,6 @@ type Props = {
|
|||
function ChannelContent(props: Props) {
|
||||
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props;
|
||||
const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{fetching && !hasContent && (
|
||||
|
@ -28,11 +27,15 @@ function ChannelContent(props: Props) {
|
|||
</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} />}
|
||||
|
||||
{hasContent && <FileList sortByHeight hideFilter fileInfos={claimsInChannel} />}
|
||||
{hasContent && <FileList noHeader uris={claimsInChannel.map(claim => claim.permanent_url)} />}
|
||||
|
||||
<Paginate
|
||||
onPageChange={page => fetchClaims(uri, page)}
|
||||
|
|
|
@ -7,24 +7,25 @@ import Gerbil from './gerbil.png';
|
|||
type Props = {
|
||||
thumbnail: ?string,
|
||||
uri: string,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
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
|
||||
const { channelName } = parseURI(uri);
|
||||
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 (
|
||||
<div
|
||||
className={classnames('channel-thumbnail', {
|
||||
[className]: !thumbnail,
|
||||
className={classnames('channel-thumbnail', className, {
|
||||
[colorClassName]: !thumbnail,
|
||||
})}
|
||||
>
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -10,7 +10,6 @@ type Props = {
|
|||
showFullPrice: boolean,
|
||||
showPlus: boolean,
|
||||
isEstimate?: boolean,
|
||||
large?: boolean,
|
||||
showLBC?: boolean,
|
||||
fee?: boolean,
|
||||
badge?: boolean,
|
||||
|
@ -27,7 +26,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
};
|
||||
|
||||
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 fullPrice = formatFullPrice(amount, 2);
|
||||
|
@ -69,7 +68,6 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
badge,
|
||||
'badge--cost': badge && amount > 0,
|
||||
'badge--free': badge && isFree,
|
||||
'badge--large': large,
|
||||
})}
|
||||
>
|
||||
{amountText}
|
||||
|
|
|
@ -12,10 +12,6 @@ const select = state => ({
|
|||
});
|
||||
|
||||
const perform = dispatch => () => ({
|
||||
completeFirstRun: () => {
|
||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
||||
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
|
||||
},
|
||||
acknowledgeEmail: () => {
|
||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
||||
},
|
||||
|
|
|
@ -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);
|
|
@ -1,14 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimsById, doSetFileListSort } from 'lbry-redux';
|
||||
import FileList from './view';
|
||||
|
||||
const select = state => ({
|
||||
claimsById: selectClaimsById(state),
|
||||
});
|
||||
const select = state => ({});
|
||||
|
||||
const perform = dispatch => ({
|
||||
setFileListSort: (page, value) => dispatch(doSetFileListSort(page, value)),
|
||||
});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
|
|
|
@ -1,165 +1,70 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { buildURI, SORT_OPTIONS } from 'lbry-redux';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import FileCard from 'component/fileCard';
|
||||
import classnames from 'classnames';
|
||||
import FileListItem from 'component/fileListItem';
|
||||
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 = {
|
||||
hideFilter: boolean,
|
||||
sortByHeight?: boolean,
|
||||
claimsById: Array<StreamClaim>,
|
||||
fileInfos: Array<FileListItem>,
|
||||
sortBy: string,
|
||||
page?: string,
|
||||
setFileListSort: (?string, string) => void,
|
||||
uris: Array<string>,
|
||||
header: React.Node,
|
||||
headerAltControls: React.Node,
|
||||
injectedItem?: React.Node,
|
||||
loading: boolean,
|
||||
noHeader?: boolean,
|
||||
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> {
|
||||
static defaultProps = {
|
||||
hideFilter: false,
|
||||
sortBy: SORT_OPTIONS.DATE_NEW,
|
||||
};
|
||||
export default function FileList(props: Props) {
|
||||
const { uris, header, headerAltControls, injectedItem, loading, persistedStorageKey, noHeader, slim, empty } = props;
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey || 'file-list-global-sort', SORT_NEW);
|
||||
const sortedUris = uris && currentSort === SORT_OLD ? uris.reverse() : uris;
|
||||
const hasUris = uris && !!uris.length;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
(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;
|
||||
}),
|
||||
};
|
||||
function handleSortChange() {
|
||||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section>
|
||||
{!hideFilter && (
|
||||
<Form>
|
||||
<FormField label={__('Sort by')} type="select" value={sortBy} onChange={this.handleSortChanged}>
|
||||
<option value={SORT_OPTIONS.DATE_NEW}>{__('Newest First')}</option>
|
||||
<option value={SORT_OPTIONS.DATE_OLD}>{__('Oldest First')}</option>
|
||||
<option value={SORT_OPTIONS.TITLE}>{__('Title')}</option>
|
||||
return (
|
||||
<section className={classnames('file-list')}>
|
||||
{!noHeader && (
|
||||
<div className="file-list__header">
|
||||
{header || (
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
type="select"
|
||||
name="file_sort"
|
||||
value={currentSort}
|
||||
onChange={handleSortChange}
|
||||
>
|
||||
<option value={SORT_NEW}>{__('Newest First')}</option>
|
||||
<option value={SORT_OLD}>{__('Oldest First')}</option>
|
||||
</FormField>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<section className="media-group--list">
|
||||
<div className="card__list">{content}</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
)}
|
||||
{loading && <Spinner light type="small" />}
|
||||
<div className="file-list__alt-controls">{headerAltControls}</div>
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileList;
|
||||
|
|
18
src/ui/component/fileListDiscover/index.js
Normal file
18
src/ui/component/fileListDiscover/index.js
Normal 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);
|
139
src/ui/component/fileListDiscover/view.jsx
Normal file
139
src/ui/component/fileListDiscover/view.jsx
Normal 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;
|
|
@ -2,8 +2,6 @@ import { connect } from 'react-redux';
|
|||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimIsPending,
|
||||
|
@ -11,25 +9,15 @@ import {
|
|||
makeSelectTitleForUri,
|
||||
makeSelectClaimIsNsfw,
|
||||
} from 'lbry-redux';
|
||||
import { selectRewardContentClaimIds } from 'lbryinc';
|
||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
||||
import { doClearContentHistoryUri } from 'redux/actions/content';
|
||||
import FileCard from './view';
|
||||
import FileListItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(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),
|
||||
position: makeSelectContentPositionForUri(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),
|
||||
|
@ -37,10 +25,9 @@ const select = (state, props) => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileCard);
|
||||
)(FileListItem);
|
132
src/ui/component/fileListItem/view.jsx
Normal file
132
src/ui/component/fileListItem/view.jsx
Normal 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);
|
|
@ -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);
|
|
@ -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;
|
18
src/ui/component/fileProperties/index.js
Normal file
18
src/ui/component/fileProperties/index.js
Normal 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);
|
31
src/ui/component/fileProperties/view.jsx
Normal file
31
src/ui/component/fileProperties/view.jsx
Normal 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>
|
||||
);
|
||||
}
|
13
src/ui/component/fileTags/index.js
Normal file
13
src/ui/component/fileTags/index.js
Normal 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);
|
49
src/ui/component/fileTags/view.jsx
Normal file
49
src/ui/component/fileTags/view.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,110 +4,100 @@ import * as React from 'react';
|
|||
import Button from 'component/button';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import WunderBar from 'component/wunderbar';
|
||||
import Icon from 'component/common/icon';
|
||||
|
||||
type Props = {
|
||||
autoUpdateDownloaded: boolean,
|
||||
balance: string,
|
||||
isUpgradeAvailable: boolean,
|
||||
roundedBalance: string,
|
||||
isBackDisabled: boolean,
|
||||
isForwardDisabled: boolean,
|
||||
back: () => void,
|
||||
forward: () => void,
|
||||
roundedBalance: number,
|
||||
downloadUpgradeRequested: any => void,
|
||||
};
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const {
|
||||
autoUpdateDownloaded,
|
||||
balance,
|
||||
downloadUpgradeRequested,
|
||||
isUpgradeAvailable,
|
||||
roundedBalance,
|
||||
back,
|
||||
isBackDisabled,
|
||||
forward,
|
||||
isForwardDisabled,
|
||||
} = props;
|
||||
const { autoUpdateDownloaded, downloadUpgradeRequested, isUpgradeAvailable, roundedBalance } = props;
|
||||
|
||||
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--lbry"
|
||||
label={__('LBRY')}
|
||||
iconRight={ICONS.LBRY}
|
||||
navigate="/"
|
||||
/>
|
||||
{/* @if TARGET='app' */}
|
||||
<div className="header__navigation-arrows">
|
||||
<div className="title-bar" />
|
||||
<div className="header__contents">
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--back"
|
||||
description={__('Navigate back')}
|
||||
onClick={() => window.history.back()}
|
||||
icon={ICONS.ARROW_LEFT}
|
||||
iconSize={15}
|
||||
className="header__navigation-item header__navigation-item--lbry"
|
||||
label={__('LBRY')}
|
||||
icon={ICONS.LBRY}
|
||||
navigate="/"
|
||||
/>
|
||||
{/* @if TARGET='app' */}
|
||||
<div className="header__navigation-arrows">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--back"
|
||||
description={__('Navigate back')}
|
||||
onClick={() => window.history.back()}
|
||||
icon={ICONS.ARROW_LEFT}
|
||||
iconSize={15}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--forward"
|
||||
description={__('Navigate forward')}
|
||||
onClick={() => window.history.forward()}
|
||||
icon={ICONS.ARROW_RIGHT}
|
||||
iconSize={15}
|
||||
/>
|
||||
</div>
|
||||
{/* @endif */}
|
||||
</div>
|
||||
|
||||
<WunderBar />
|
||||
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
label={
|
||||
roundedBalance > 0 ? (
|
||||
<React.Fragment>
|
||||
{roundedBalance} <LbcSymbol />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
__('Account')
|
||||
)
|
||||
}
|
||||
icon={ICONS.ACCOUNT}
|
||||
navigate="/$/account"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--forward"
|
||||
description={__('Navigate forward')}
|
||||
onClick={() => window.history.forward()}
|
||||
icon={ICONS.ARROW_RIGHT}
|
||||
iconSize={15}
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
description={__('Publish content')}
|
||||
icon={ICONS.UPLOAD}
|
||||
iconSize={24}
|
||||
navigate="/$/publish"
|
||||
/>
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
{showUpgradeButton && (
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade"
|
||||
icon={ICONS.DOWNLOAD}
|
||||
iconSize={24}
|
||||
label={__('Upgrade App')}
|
||||
onClick={downloadUpgradeRequested}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
icon={ICONS.SETTINGS}
|
||||
iconSize={24}
|
||||
navigate="/$/settings"
|
||||
/>
|
||||
</div>
|
||||
{/* @endif */}
|
||||
</div>
|
||||
|
||||
<WunderBar />
|
||||
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--menu"
|
||||
description={__('Menu')}
|
||||
icon={ICONS.MENU}
|
||||
iconSize={15}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
description={__('Your wallet')}
|
||||
title={`Your balance is ${balance} LBRY Credits`}
|
||||
label={
|
||||
<React.Fragment>
|
||||
{roundedBalance} <LbcSymbol />
|
||||
</React.Fragment>
|
||||
}
|
||||
navigate="/$/account"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
description={__('Publish content')}
|
||||
icon={ICONS.UPLOAD}
|
||||
iconSize={24}
|
||||
label={isUpgradeAvailable ? '' : __('Publish')}
|
||||
navigate="/$/publish"
|
||||
/>
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
|
||||
{showUpgradeButton && (
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade"
|
||||
icon={ICONS.DOWNLOAD}
|
||||
iconSize={24}
|
||||
label={__('Upgrade App')}
|
||||
onClick={downloadUpgradeRequested}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function NavigationHistoryRecent(props: Props) {
|
|||
))}
|
||||
</section>
|
||||
<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>
|
||||
) : null;
|
||||
|
|
|
@ -9,7 +9,6 @@ const LOADER_TIMEOUT = 1000;
|
|||
type Props = {
|
||||
children: React.Node | Array<React.Node>,
|
||||
pageTitle: ?string,
|
||||
notContained: ?boolean, // No max-width, but keep the padding
|
||||
loading: ?boolean,
|
||||
className: ?string,
|
||||
};
|
||||
|
@ -69,16 +68,11 @@ class Page extends React.PureComponent<Props, State> {
|
|||
loaderTimeout: ?TimeoutID;
|
||||
|
||||
render() {
|
||||
const { children, notContained, loading, className } = this.props;
|
||||
const { children, loading, className } = this.props;
|
||||
const { showLoader } = this.state;
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classnames('main', className, {
|
||||
'main--contained': !notContained,
|
||||
'main--not-contained': notContained,
|
||||
})}
|
||||
>
|
||||
<main className={classnames('main', className)}>
|
||||
{!loading && children}
|
||||
{showLoader && (
|
||||
<div className="main--empty">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import FileTile from 'component/fileTile';
|
||||
import FileList from 'component/fileList';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -51,15 +51,14 @@ export default class RecommendedContent extends React.PureComponent<Props> {
|
|||
const { recommendedContent, isSearching } = this.props;
|
||||
|
||||
return (
|
||||
<section className="media-group--list-recommended">
|
||||
<span>Related</span>
|
||||
{recommendedContent &&
|
||||
recommendedContent.map(recommendedUri => (
|
||||
<FileTile hideNoResult size="small" key={recommendedUri} uri={recommendedUri} />
|
||||
))}
|
||||
{recommendedContent && !recommendedContent.length && !isSearching && (
|
||||
<div className="media__subtitle">No related content found</div>
|
||||
)}
|
||||
<section className="card">
|
||||
<FileList
|
||||
slim
|
||||
loading={isSearching}
|
||||
uris={recommendedContent}
|
||||
header={<span>Related</span>}
|
||||
empty={<div className="empty">{__('No related content found')}</div>}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -43,7 +43,8 @@ class RewardSummary extends React.Component<Props> {
|
|||
<React.Fragment>
|
||||
{__('There are no rewards available at this time, please check back later')}.
|
||||
</React.Fragment>
|
||||
))}
|
||||
))}{' '}
|
||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
@ -55,11 +56,6 @@ class RewardSummary extends React.Component<Props> {
|
|||
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
|
||||
/>
|
||||
</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>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,8 @@ import SearchPage from 'page/search';
|
|||
import UserHistoryPage from 'page/userHistory';
|
||||
import SendCreditsPage from 'page/sendCredits';
|
||||
import NavigationHistory from 'page/navigationHistory';
|
||||
import TagsPage from 'page/tags';
|
||||
import TagsEditPage from 'page/tagsEdit';
|
||||
|
||||
const Scroll = withRouter(function ScrollWrapper(props) {
|
||||
const { pathname } = props.location;
|
||||
|
@ -50,11 +52,12 @@ export default function AppRouter() {
|
|||
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
||||
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
|
||||
<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.SEND}`} exact component={SendCreditsPage} />
|
||||
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} />
|
||||
<Route path={`/$/${PAGES.HISTORY}/all`} exact component={NavigationHistory} />
|
||||
<Route path={`/$/${PAGES.LIBRARY}/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 */}
|
||||
<Route path="/:claimName" exact component={ShowPage} />
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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 { selectSearchOptionsExpanded } from 'redux/selectors/app';
|
||||
import analytics from 'analytics';
|
||||
import SearchOptions from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
@ -14,24 +13,6 @@ const select = state => ({
|
|||
const perform = dispatch => ({
|
||||
setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
|
||||
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(
|
||||
|
|
|
@ -16,39 +16,20 @@ type Props = {
|
|||
options: {},
|
||||
expanded: boolean,
|
||||
toggleSearchExpanded: () => void,
|
||||
query: string,
|
||||
onFeedbackPositive: string => void,
|
||||
onFeedbackNegative: string => void,
|
||||
};
|
||||
|
||||
const SearchOptions = (props: Props) => {
|
||||
const {
|
||||
options,
|
||||
setSearchOption,
|
||||
expanded,
|
||||
toggleSearchExpanded,
|
||||
query,
|
||||
onFeedbackPositive,
|
||||
onFeedbackNegative,
|
||||
} = props;
|
||||
const { options, setSearchOption, expanded, toggleSearchExpanded } = props;
|
||||
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
|
||||
|
||||
return (
|
||||
<div className="search__options-wrapper">
|
||||
<div className="card--space-between">
|
||||
<Button
|
||||
button="alt"
|
||||
label={__('FILTER')}
|
||||
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
|
||||
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>
|
||||
<div>
|
||||
<Button
|
||||
button="alt"
|
||||
label={__('FILTER')}
|
||||
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
|
||||
onClick={toggleSearchExpanded}
|
||||
/>
|
||||
<ExpandableOptions pose={expanded ? 'show' : 'hide'}>
|
||||
{expanded && (
|
||||
<Form className="card__content search__options">
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectUnreadAmount } from 'redux/selectors/subscriptions';
|
||||
import { selectShouldShowInviteGuide } from 'redux/selectors/app';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { selectFollowedTags } from 'lbry-redux';
|
||||
import SideBar from './view';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
|
||||
const select = state => ({
|
||||
unreadSubscriptionTotal: selectUnreadAmount(state),
|
||||
subscriptions: selectSubscriptions(state),
|
||||
followedTags: selectFollowedTags(state),
|
||||
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
||||
shouldShowInviteGuide: selectShouldShowInviteGuide(state),
|
||||
});
|
||||
|
||||
const perform = () => ({});
|
||||
|
|
|
@ -3,107 +3,74 @@ import * as PAGES from 'constants/pages';
|
|||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import Tag from 'component/tag';
|
||||
|
||||
type Props = {
|
||||
unreadSubscriptionTotal: number,
|
||||
shouldShowInviteGuide: string,
|
||||
subscriptions: Array<Subscription>,
|
||||
followedTags: Array<Tag>,
|
||||
};
|
||||
|
||||
class SideBar extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { unreadSubscriptionTotal, shouldShowInviteGuide } = this.props;
|
||||
const buildLink = (path, label, icon, guide) => ({
|
||||
navigate: path ? `$/${path}` : '/',
|
||||
label,
|
||||
icon,
|
||||
guide,
|
||||
});
|
||||
function SideBar(props: Props) {
|
||||
const { subscriptions, followedTags } = props;
|
||||
const buildLink = (path, label, icon, guide) => ({
|
||||
navigate: path ? `$/${path}` : '/',
|
||||
label,
|
||||
icon,
|
||||
guide,
|
||||
});
|
||||
|
||||
const renderLink = (linkProps, index) => {
|
||||
const { guide } = linkProps;
|
||||
const renderLink = 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 (
|
||||
<li key={index}>
|
||||
{guide ? (
|
||||
<Tooltip key={guide} alwaysVisible direction="right" body={guide}>
|
||||
{inner}
|
||||
</Tooltip>
|
||||
) : (
|
||||
inner
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className="navigation-wrapper">
|
||||
<nav className="navigation">
|
||||
<ul className="navigation__links">
|
||||
{[
|
||||
{
|
||||
...buildLink(null, __('Discover'), ICONS.DISCOVER),
|
||||
...buildLink(null, __('Home'), ICONS.HOME),
|
||||
},
|
||||
{
|
||||
...buildLink(
|
||||
PAGES.SUBSCRIPTIONS,
|
||||
`${__('Subscriptions')} ${unreadSubscriptionTotal > 0 ? '(' + unreadSubscriptionTotal + ')' : ''}`,
|
||||
ICONS.SUBSCRIPTION
|
||||
),
|
||||
...buildLink(PAGES.SUBSCRIPTIONS, __('Subscriptions'), ICONS.SUBSCRIPTION),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.HISTORY, __('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),
|
||||
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.DOWNLOAD),
|
||||
},
|
||||
].map(renderLink)}
|
||||
</ul>
|
||||
|
||||
<ul className="navigation__links navigation__links--bottom">
|
||||
{[
|
||||
{
|
||||
...buildLink(PAGES.HELP, __('Help'), ICONS.HELP),
|
||||
},
|
||||
].map(renderLink)}
|
||||
<Button
|
||||
navigate="/$/tags/edit"
|
||||
iconRight={ICONS.SETTINGS}
|
||||
className="navigation__link--title navigation__link"
|
||||
activeClass="navigation__link--active"
|
||||
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>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideBar;
|
||||
|
|
|
@ -66,8 +66,8 @@ class Spinner extends PureComponent<Props, State> {
|
|||
className={classnames('spinner', {
|
||||
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
|
||||
'spinner--light': !dark && (light || theme === DARK_THEME),
|
||||
'spinner--splash': type === 'splash',
|
||||
'spinner--small': type === 'small',
|
||||
'spinner--splash': type === 'splash',
|
||||
})}
|
||||
>
|
||||
<div className="rect rect1" />
|
||||
|
|
|
@ -22,7 +22,7 @@ type Props = {
|
|||
buttonStyle: string,
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
export default function SubscribeButton(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
doChannelSubscribe,
|
||||
|
@ -36,14 +36,14 @@ export default (props: Props) => {
|
|||
} = props;
|
||||
|
||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
||||
const subscriptionLabel = isSubscribed ? __('Subscribed') : __('Subscribe');
|
||||
|
||||
const { claimName } = parseURI(uri);
|
||||
|
||||
return (
|
||||
<Button
|
||||
iconColor="red"
|
||||
icon={isSubscribed ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIPTION}
|
||||
icon={ICONS.SUBSCRIPTION}
|
||||
button={buttonStyle || 'alt'}
|
||||
label={subscriptionLabel}
|
||||
onClick={e => {
|
||||
|
@ -64,4 +64,4 @@ export default (props: Props) => {
|
|||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
11
src/ui/component/tag/index.js
Normal file
11
src/ui/component/tag/index.js
Normal 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);
|
35
src/ui/component/tag/view.jsx
Normal file
35
src/ui/component/tag/view.jsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
25
src/ui/component/tagsSearch/index.js
Normal file
25
src/ui/component/tagsSearch/index.js
Normal 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);
|
69
src/ui/component/tagsSearch/view.jsx
Normal file
69
src/ui/component/tagsSearch/view.jsx
Normal 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>
|
||||
);
|
||||
}
|
25
src/ui/component/tagsSelect/index.js
Normal file
25
src/ui/component/tagsSelect/index.js
Normal 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);
|
63
src/ui/component/tagsSelect/view.jsx
Normal file
63
src/ui/component/tagsSelect/view.jsx
Normal 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>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -41,7 +41,7 @@ class TransactionListRecent extends React.PureComponent<Props> {
|
|||
|
||||
{!fetchingTransactions && !hasTransactions && (
|
||||
<div className="card__content">
|
||||
<p className="card__subtitle">{__('No transactions... yet.')}</p>
|
||||
<p className="card__subtitle">{__('No transactions.')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ type Props = {
|
|||
isResolvingUri: boolean,
|
||||
channelUri: ?string,
|
||||
link: ?boolean,
|
||||
claim: ?StreamClaim,
|
||||
channelClaim: ?ChannelClaim,
|
||||
claim: ?Claim,
|
||||
// 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?
|
||||
resolveUri: string => void,
|
||||
|
@ -16,12 +15,12 @@ type Props = {
|
|||
};
|
||||
|
||||
class UriIndicator extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.resolve(nextProps);
|
||||
componentDidUpdate() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
resolve = (props: Props) => {
|
||||
|
@ -39,27 +38,35 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
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>;
|
||||
}
|
||||
|
||||
const { name, claim_id: claimId } = claim.signing_channel;
|
||||
let channelLink;
|
||||
if (claim.is_channel_signature_valid) {
|
||||
channelLink = link ? buildURI({ channelName: name, claimId }) : false;
|
||||
const channelClaim = isChannelClaim ? claim : claim.signing_channel;
|
||||
|
||||
if (channelClaim) {
|
||||
const { name, claim_id: claimId } = channelClaim;
|
||||
let channelLink;
|
||||
if (claim.is_channel_signature_valid) {
|
||||
channelLink = link ? buildURI({ channelName: name, claimId }) : false;
|
||||
}
|
||||
|
||||
const inner = <span className="channel-name">{name}</span>;
|
||||
|
||||
if (!channelLink) {
|
||||
return inner;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className="button--uri-indicator" navigate={channelLink}>
|
||||
{inner}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inner = <span className="channel-name">{name}</span>;
|
||||
|
||||
if (!channelLink) {
|
||||
return inner;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className="button--uri-indicator" navigate={channelLink}>
|
||||
{inner}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,6 @@ class UserEmailNew extends React.PureComponent<Props, State> {
|
|||
/>
|
||||
</Form>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,11 @@ const WalletBalance = (props: Props) => {
|
|||
</header>
|
||||
<div className="card__content">
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@ export const SHOW_MODAL = 'SHOW_MODAL';
|
|||
export const HIDE_MODAL = 'HIDE_MODAL';
|
||||
export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED';
|
||||
export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED';
|
||||
export const ENNNHHHAAANNNCEEE = 'ENNNHHHAAANNNCEEE';
|
||||
|
||||
// Navigation
|
||||
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_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
|
||||
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
|
||||
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
||||
|
|
|
@ -13,6 +13,7 @@ export const DOWNLOAD = 'Download';
|
|||
export const UPLOAD = 'UploadCloud';
|
||||
export const PUBLISHED = 'Cloud';
|
||||
export const CLOSE = 'X';
|
||||
export const ADD = 'Plus';
|
||||
export const EDIT = 'Edit3';
|
||||
export const DELETE = 'Trash';
|
||||
export const REPORT = 'Flag';
|
||||
|
|
|
@ -4,7 +4,7 @@ export const CHANNEL = 'channel';
|
|||
export const DISCOVER = 'discover';
|
||||
export const DOWNLOADED = 'downloaded';
|
||||
export const HELP = 'help';
|
||||
export const HISTORY = 'history';
|
||||
export const LIBRARY = 'library';
|
||||
export const INVITE = 'invite';
|
||||
export const PUBLISH = 'publish';
|
||||
export const PUBLISHED = 'published';
|
||||
|
@ -18,3 +18,4 @@ export const ACCOUNT = 'account';
|
|||
export const SUBSCRIPTIONS = 'subscriptions';
|
||||
export const SEARCH = 'search';
|
||||
export const TRANSACTIONS = 'transactions';
|
||||
export const TAGS = 'tags';
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
|
||||
export const NEW_USER_ACKNOWLEDGED = 'welcome_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 LANGUAGE = 'language';
|
||||
export const SHOW_NSFW = 'showNsfw';
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import * as React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import Button from 'component/button';
|
||||
import app from 'app';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type ModalProps = {
|
||||
|
@ -20,7 +19,6 @@ type ModalProps = {
|
|||
extraContent?: React.Node,
|
||||
expandButtonLabel?: string,
|
||||
hideButtonLabel?: string,
|
||||
fullScreen: boolean,
|
||||
title?: string | React.Node,
|
||||
};
|
||||
|
||||
|
@ -32,7 +30,6 @@ export class Modal extends React.PureComponent<ModalProps> {
|
|||
abortButtonLabel: __('Cancel'),
|
||||
confirmButtonDisabled: false,
|
||||
abortButtonDisabled: false,
|
||||
fullScreen: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -45,7 +42,6 @@ export class Modal extends React.PureComponent<ModalProps> {
|
|||
abortButtonLabel,
|
||||
abortButtonDisabled,
|
||||
onAborted,
|
||||
fullScreen,
|
||||
className,
|
||||
title,
|
||||
...modalProps
|
||||
|
@ -54,10 +50,7 @@ export class Modal extends React.PureComponent<ModalProps> {
|
|||
<ReactModal
|
||||
{...modalProps}
|
||||
onRequestClose={onAborted || onConfirmed}
|
||||
className={classnames('card', className, {
|
||||
modal: !fullScreen,
|
||||
'modal--fullscreen': fullScreen,
|
||||
})}
|
||||
className={classnames('card card--modal modal', className)}
|
||||
overlayClassName="modal-overlay"
|
||||
>
|
||||
{title && (
|
||||
|
|
|
@ -40,7 +40,7 @@ class ModalAffirmPurchase extends React.PureComponent<Props> {
|
|||
onAborted={cancelPurchase}
|
||||
>
|
||||
<section className="card__content">
|
||||
<p>
|
||||
<p className="card__subtitle">
|
||||
{__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '}
|
||||
<strong>
|
||||
<FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} />
|
||||
|
|
|
@ -50,8 +50,6 @@ function ModalAutoGenerateThumbnail(props: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log('resized');
|
||||
|
||||
const fixedWidth = 450;
|
||||
const videoWidth = player.videoWidth;
|
||||
const videoHeight = player.videoHeight;
|
||||
|
|
|
@ -16,6 +16,8 @@ const WalletPage = () => (
|
|||
<WalletBalance />
|
||||
<RewardSummary />
|
||||
</div>
|
||||
<WalletAddress />
|
||||
<WalletSend />
|
||||
<TransactionListRecent />
|
||||
</div>
|
||||
</Page>
|
||||
|
|
|
@ -49,42 +49,44 @@ function ChannelPage(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Page notContained className="main--no-padding-top">
|
||||
<header className="channel-cover main__item--extend-outside">
|
||||
{cover && <img className="channel-cover__custom" src={cover} />}
|
||||
<Page>
|
||||
<div className="card">
|
||||
<header className="channel-cover">
|
||||
{cover && <img className="channel-cover__custom" src={cover} />}
|
||||
|
||||
<div className="channel__primary-info">
|
||||
<ChannelThumbnail uri={uri} />
|
||||
<div className="channel__primary-info">
|
||||
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} />
|
||||
|
||||
<div>
|
||||
<h1 className="channel__title">{title || channelName}</h1>
|
||||
<h2 className="channel__url">
|
||||
{claimName}
|
||||
{claimId && `#${claimId}`}
|
||||
</h2>
|
||||
<div>
|
||||
<h1 className="channel__title">{title || channelName}</h1>
|
||||
<h2 className="channel__url">
|
||||
{claimName}
|
||||
{claimId && `#${claimId}`}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="main__item--extend-outside tabs__list--channel-page">
|
||||
<Tab>{__('Content')}</Tab>
|
||||
<Tab>{__('About')}</Tab>
|
||||
<div className="card__actions">
|
||||
<ShareButton uri={uri} />
|
||||
<SubscribeButton uri={uri} />
|
||||
</div>
|
||||
</TabList>
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab>{__('Content')}</Tab>
|
||||
<Tab>{__('About')}</Tab>
|
||||
<div className="card__actions">
|
||||
<ShareButton uri={uri} />
|
||||
<SubscribeButton uri={uri} />
|
||||
</div>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<ChannelContent uri={uri} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelAbout uri={uri} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<TabPanels className="channel__data">
|
||||
<TabPanel>
|
||||
<ChannelContent uri={uri} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelAbout uri={uri} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchRewardedContent,
|
||||
doRewardList,
|
||||
selectFeaturedUris,
|
||||
doFetchFeaturedUris,
|
||||
selectFetchingFeaturedUris,
|
||||
} from 'lbryinc';
|
||||
import { selectFollowedTags } from 'lbry-redux';
|
||||
import DiscoverPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
featuredUris: selectFeaturedUris(state),
|
||||
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
|
||||
followedTags: selectFollowedTags(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
});
|
||||
const perform = {};
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
|
|
|
@ -1,82 +1,24 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import FileListDiscover from 'component/fileListDiscover';
|
||||
import TagsSelect from 'component/tagsSelect';
|
||||
import Page from 'component/page';
|
||||
import CategoryList from 'component/categoryList';
|
||||
import FirstRun from 'component/firstRun';
|
||||
|
||||
type Props = {
|
||||
fetchFeaturedUris: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
fetchRewards: () => void,
|
||||
fetchingFeaturedUris: boolean,
|
||||
featuredUris: {},
|
||||
followedTags: Array<Tag>,
|
||||
};
|
||||
|
||||
class DiscoverPage extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
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 (
|
||||
<Page notContained isLoading={!hasContent && fetchingFeaturedUris} className="main--no-padding">
|
||||
<FirstRun />
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(category => (
|
||||
<CategoryList
|
||||
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>
|
||||
);
|
||||
}
|
||||
function DiscoverPage(props: Props) {
|
||||
const { followedTags } = props;
|
||||
return (
|
||||
<Page className="card">
|
||||
<FileListDiscover
|
||||
personal
|
||||
tags={followedTags.map(tag => tag.name)}
|
||||
injectedItem={<TagsSelect showClose title={__('Make This Your Own')} />}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiscoverPage;
|
||||
|
|
|
@ -108,15 +108,15 @@ class FilePage extends React.Component<Props> {
|
|||
fetchViewCount(claim.claim_id);
|
||||
}
|
||||
|
||||
if (prevProps.uri !== uri) {
|
||||
setViewed(uri);
|
||||
}
|
||||
|
||||
// @if TARGET='app'
|
||||
if (fileInfo === undefined) {
|
||||
fetchFileInfo(uri);
|
||||
}
|
||||
// @endif
|
||||
|
||||
if (prevProps.uri !== uri) {
|
||||
setViewed(uri);
|
||||
}
|
||||
}
|
||||
|
||||
removeFromSubscriptionNotifications() {
|
||||
|
@ -148,7 +148,8 @@ class FilePage extends React.Component<Props> {
|
|||
} = this.props;
|
||||
|
||||
// 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 isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
|
||||
const shouldObscureThumbnail = obscureNsfw && nsfw;
|
||||
|
@ -179,23 +180,12 @@ class FilePage extends React.Component<Props> {
|
|||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
||||
|
||||
return (
|
||||
<Page notContained className="main--file-page">
|
||||
<Page className="main--file-page">
|
||||
<div className="grid-area--content">
|
||||
<Button
|
||||
className="media__uri"
|
||||
button="alt"
|
||||
label={uri}
|
||||
onClick={() => {
|
||||
clipboard.writeText(uri);
|
||||
showToast({
|
||||
message: __('Text copied'),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{!fileInfo && insufficientCredits && (
|
||||
<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')} />{' '}
|
||||
{__('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>
|
||||
))}
|
||||
<Button
|
||||
className="media__uri"
|
||||
button="alt"
|
||||
label={uri}
|
||||
onClick={() => {
|
||||
clipboard.writeText(uri);
|
||||
showToast({
|
||||
message: __('Copied'),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid-area--info media__content media__content--large">
|
||||
<h1 className="media__title media__title--large">{title}</h1>
|
||||
|
||||
<div className="media__properties media__properties--large">
|
||||
<div className="file-properties">
|
||||
{isRewardContent && (
|
||||
<Icon
|
||||
size={20}
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectFileInfosDownloaded,
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingFileList,
|
||||
selectFileListDownloadedSort,
|
||||
} from 'lbry-redux';
|
||||
import { selectDownloadedUris, selectIsFetchingFileList } from 'lbry-redux';
|
||||
import FileListDownloaded from './view';
|
||||
|
||||
const select = state => ({
|
||||
fileInfos: selectFileInfosDownloaded(state),
|
||||
downloadedUris: selectDownloadedUris(state),
|
||||
fetching: selectIsFetchingFileList(state),
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
sortBy: selectFileListDownloadedSort(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -2,44 +2,41 @@
|
|||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import FileList from 'component/fileList';
|
||||
import Page from 'component/page';
|
||||
import { PAGES } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
fetching: boolean,
|
||||
fileInfos: {},
|
||||
sortBy: string,
|
||||
downloadedUris: Array<string>,
|
||||
};
|
||||
|
||||
class FileListDownloaded extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { fetching, fileInfos, sortBy } = this.props;
|
||||
const hasDownloads = fileInfos && Object.values(fileInfos).length > 0;
|
||||
function FileListDownloaded(props: Props) {
|
||||
const { fetching, downloadedUris } = props;
|
||||
const hasDownloads = !!downloadedUris.length;
|
||||
|
||||
return (
|
||||
// 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
|
||||
<React.Fragment>
|
||||
{hasDownloads ? (
|
||||
<FileList fileInfos={fileInfos} sortBy={sortBy} page={PAGES.DOWNLOADED} />
|
||||
) : (
|
||||
<div className="main--empty">
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">{__("You haven't downloaded anything from LBRY yet.")}</h2>
|
||||
</header>
|
||||
return (
|
||||
// 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
|
||||
<React.Fragment>
|
||||
{hasDownloads ? (
|
||||
<div className="card">
|
||||
<FileList persistedStorageKey="file-list-downloaded" uris={downloadedUris} loading={fetching} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="main--empty">
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">{__("You haven't downloaded anything from LBRY yet.")}</h2>
|
||||
</header>
|
||||
|
||||
<div className="card__content">
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button button="primary" navigate="/" label={__('Explore new content')} />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button button="primary" navigate="/" label={__('Explore new content')} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileListDownloaded;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
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 FileListPublished from './view';
|
||||
|
||||
const select = state => ({
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
fetching: selectIsFetchingClaimListMine(state),
|
||||
sortBy: selectFileListPublishedSort(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -1,47 +1,49 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from 'component/button';
|
||||
import FileList from 'component/fileList';
|
||||
import Page from 'component/page';
|
||||
import { PAGES } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
claims: Array<StreamClaim>,
|
||||
checkPendingPublishes: () => void,
|
||||
fetching: boolean,
|
||||
sortBy: string,
|
||||
};
|
||||
|
||||
class FileListPublished extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
const { checkPendingPublishes } = this.props;
|
||||
function FileListPublished(props: Props) {
|
||||
const { checkPendingPublishes, fetching, claims } = props;
|
||||
|
||||
useEffect(() => {
|
||||
checkPendingPublishes();
|
||||
}
|
||||
}, [checkPendingPublishes]);
|
||||
|
||||
render() {
|
||||
const { fetching, claims, sortBy } = this.props;
|
||||
return (
|
||||
<Page notContained loading={fetching}>
|
||||
{claims && claims.length ? (
|
||||
<FileList checkPending fileInfos={claims} sortByHeight sortBy={sortBy} page={PAGES.PUBLISHED} />
|
||||
) : (
|
||||
<div className="main--empty">
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">{__("It looks like you haven't published anything to LBRY yet.")}</h2>
|
||||
</header>
|
||||
return (
|
||||
<Page notContained loading={fetching}>
|
||||
{claims && claims.length ? (
|
||||
<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">
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">{__("It looks like you haven't published anything to LBRY yet.")}</h2>
|
||||
</header>
|
||||
|
||||
<div className="card__content">
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button button="primary" navigate="/$/publish" label={__('Publish something new')} />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button button="primary" navigate="/$/publish" label={__('Publish something new')} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileListPublished;
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
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';
|
||||
|
||||
const select = state => ({
|
||||
isSearching: selectIsSearching(state),
|
||||
uris: makeSelectSearchUris(makeSelectQueryWithOptions()(state))(state),
|
||||
});
|
||||
|
||||
const perform = {
|
||||
doSearch,
|
||||
};
|
||||
const perform = dispatch => ({
|
||||
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(
|
||||
select,
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useEffect, Fragment } from 'react';
|
||||
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
|
||||
import FileTile from 'component/fileTile';
|
||||
import ChannelTile from 'component/channelTile';
|
||||
import FileListSearch from 'component/fileListSearch';
|
||||
import { isURIValid, normalizeURI } from 'lbry-redux';
|
||||
import FileListItem from 'component/fileListItem';
|
||||
import FileList from 'component/fileList';
|
||||
import Page from 'component/page';
|
||||
import SearchOptions from 'component/searchOptions';
|
||||
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) {
|
||||
const {
|
||||
doSearch,
|
||||
uris,
|
||||
onFeedbackPositive,
|
||||
onFeedbackNegative,
|
||||
location: { search },
|
||||
} = props;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
@ -20,10 +29,8 @@ export default function SearchPage(props: Props) {
|
|||
const isValid = isURIValid(urlQuery);
|
||||
|
||||
let uri;
|
||||
let isChannel;
|
||||
if (isValid) {
|
||||
uri = normalizeURI(urlQuery);
|
||||
({ isChannel } = parseURI(uri));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -42,19 +49,34 @@ export default function SearchPage(props: Props) {
|
|||
<Button button="alt" navigate={uri} className="media__uri">
|
||||
{uri}
|
||||
</Button>
|
||||
{isChannel ? (
|
||||
<ChannelTile size="large" isSearchResult uri={uri} />
|
||||
) : (
|
||||
<FileTile size="large" isSearchResult displayHiddenMessage uri={uri} />
|
||||
)}
|
||||
<FileListItem uri={uri} large />
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="search__results-wrapper">
|
||||
<SearchOptions />
|
||||
<FileListSearch query={urlQuery} />
|
||||
<div className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div>
|
||||
<div className="card">
|
||||
<FileList
|
||||
uris={uris}
|
||||
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 className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
@ -49,7 +49,7 @@ class ShowPage extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
innerContent = (
|
||||
<Page notContained>
|
||||
<Page>
|
||||
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
|
||||
{!isResolvingUri && <span className="empty">{__("There's nothing available at this location.")}</span>}
|
||||
</Page>
|
||||
|
|
|
@ -1,45 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import * as settings from 'constants/settings';
|
||||
import {
|
||||
selectSubscriptionClaims,
|
||||
selectSubscriptions,
|
||||
selectSubscriptionsBeingFetched,
|
||||
selectIsFetchingSubscriptions,
|
||||
selectUnreadSubscriptions,
|
||||
selectViewMode,
|
||||
selectFirstRunCompleted,
|
||||
selectshowSuggestedSubs,
|
||||
selectSuggestedChannels,
|
||||
} from 'redux/selectors/subscriptions';
|
||||
import {
|
||||
doFetchMySubscriptions,
|
||||
doSetViewMode,
|
||||
doFetchRecommendedSubscriptions,
|
||||
doCompleteFirstRun,
|
||||
doShowSuggestedSubs,
|
||||
} from 'redux/actions/subscriptions';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doFetchMySubscriptions, doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
|
||||
import SubscriptionsPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
loading: selectIsFetchingSubscriptions(state) || Boolean(Object.keys(selectSubscriptionsBeingFetched(state)).length),
|
||||
subscribedChannels: selectSubscriptions(state),
|
||||
autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state),
|
||||
allSubscriptions: selectSubscriptionClaims(state),
|
||||
unreadSubscriptions: selectUnreadSubscriptions(state),
|
||||
viewMode: selectViewMode(state),
|
||||
firstRunCompleted: selectFirstRunCompleted(state),
|
||||
showSuggestedSubs: selectshowSuggestedSubs(state),
|
||||
subscriptionContent: selectSubscriptionClaims(state),
|
||||
suggestedSubscriptions: selectSuggestedChannels(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
{
|
||||
doFetchMySubscriptions,
|
||||
doSetClientSetting,
|
||||
doSetViewMode,
|
||||
doFetchRecommendedSubscriptions,
|
||||
doCompleteFirstRun,
|
||||
doShowSuggestedSubs,
|
||||
}
|
||||
)(SubscriptionsPage);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,105 +1,55 @@
|
|||
// @flow
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Page from 'component/page';
|
||||
import FirstRun from './internal/first-run';
|
||||
import UserSubscriptions from './internal/user-subscriptions';
|
||||
import FileList from 'component/fileList';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
subscribedChannels: Array<string>, // The channels a user is subscribed to
|
||||
unreadSubscriptions: Array<{
|
||||
channel: string,
|
||||
uris: Array<string>,
|
||||
}>,
|
||||
allSubscriptions: Array<{ uri: string, ...StreamClaim }>,
|
||||
subscriptionContent: Array<{ uri: string, ...StreamClaim }>,
|
||||
suggestedSubscriptions: Array<{ uri: string }>,
|
||||
loading: boolean,
|
||||
autoDownload: boolean,
|
||||
viewMode: ViewMode,
|
||||
doSetViewMode: ViewMode => void,
|
||||
doFetchMySubscriptions: () => void,
|
||||
doSetClientSetting: (string, boolean) => void,
|
||||
doFetchRecommendedSubscriptions: () => void,
|
||||
loadingSuggested: boolean,
|
||||
firstRunCompleted: boolean,
|
||||
doCompleteFirstRun: () => void,
|
||||
doShowSuggestedSubs: () => void,
|
||||
showSuggestedSubs: boolean,
|
||||
};
|
||||
|
||||
export default class SubscriptionsPage extends PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
(this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
doFetchMySubscriptions,
|
||||
doFetchRecommendedSubscriptions,
|
||||
allSubscriptions,
|
||||
firstRunCompleted,
|
||||
doShowSuggestedSubs,
|
||||
} = this.props;
|
||||
export default function SubscriptionsPage(props: Props) {
|
||||
const {
|
||||
subscriptionContent,
|
||||
subscribedChannels,
|
||||
doFetchMySubscriptions,
|
||||
doFetchRecommendedSubscriptions,
|
||||
suggestedSubscriptions,
|
||||
loading,
|
||||
} = props;
|
||||
const hasSubscriptions = !!subscribedChannels.length;
|
||||
const [showSuggested, setShowSuggested] = useState(!hasSubscriptions);
|
||||
|
||||
useEffect(() => {
|
||||
doFetchMySubscriptions();
|
||||
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 (
|
||||
// Only pass in the loading prop if there are no subscriptions
|
||||
// If there are any, let the page update in the background
|
||||
// The loading prop removes children and shows a loading spinner
|
||||
<Page notContained loading={loading && !subscribedChannels} className="main--no-padding-top">
|
||||
{firstRunCompleted ? (
|
||||
<UserSubscriptions
|
||||
viewMode={viewMode}
|
||||
doSetViewMode={doSetViewMode}
|
||||
hasSubscriptions={numberOfSubscriptions > 0}
|
||||
subscriptions={allSubscriptions}
|
||||
autoDownload={autoDownload}
|
||||
onChangeAutoDownload={this.onAutoDownloadChange}
|
||||
unreadSubscriptions={unreadSubscriptions}
|
||||
loadingSuggested={loadingSuggested}
|
||||
/>
|
||||
) : (
|
||||
<FirstRun
|
||||
showSuggested={showSuggestedSubs}
|
||||
doShowSuggestedSubs={doShowSuggestedSubs}
|
||||
loadingSuggested={loadingSuggested}
|
||||
numberOfSubscriptions={numberOfSubscriptions}
|
||||
onFinish={doCompleteFirstRun}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Page>
|
||||
<div className="card">
|
||||
<FileList
|
||||
loading={loading}
|
||||
header={<h1>{showSuggested ? __('Discover New Channels') : __('Latest From Your Subscriptions')}</h1>}
|
||||
headerAltControls={
|
||||
<Button
|
||||
button="alt"
|
||||
label={showSuggested ? hasSubscriptions && __('View Your Subscriptions') : __('Find New Channels')}
|
||||
onClick={() => setShowSuggested(!showSuggested)}
|
||||
/>
|
||||
}
|
||||
uris={
|
||||
showSuggested
|
||||
? suggestedSubscriptions.map(sub => sub.uri)
|
||||
: subscriptionContent.map(sub => sub.permanent_url)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
14
src/ui/page/tags/index.js
Normal file
14
src/ui/page/tags/index.js
Normal 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
28
src/ui/page/tags/view.jsx
Normal 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;
|
14
src/ui/page/tagsEdit/index.js
Normal file
14
src/ui/page/tagsEdit/index.js
Normal 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);
|
18
src/ui/page/tagsEdit/view.jsx
Normal file
18
src/ui/page/tagsEdit/view.jsx
Normal 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;
|
|
@ -1,6 +1,13 @@
|
|||
import { combineReducers } from 'redux';
|
||||
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 appReducer from 'redux/reducers/app';
|
||||
import availabilityReducer from 'redux/reducers/availability';
|
||||
|
@ -27,6 +34,7 @@ export default history =>
|
|||
settings: settingsReducer,
|
||||
stats: statsReducer,
|
||||
subscriptions: subscriptionsReducer,
|
||||
tags: tagsReducer,
|
||||
user: userReducer,
|
||||
wallet: walletReducer,
|
||||
});
|
||||
|
|
|
@ -391,12 +391,6 @@ export function doConditionalAuthNavigate(newSession) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doToggleEnhancedLayout() {
|
||||
return {
|
||||
type: ACTIONS.ENNNHHHAAANNNCEEE,
|
||||
};
|
||||
}
|
||||
|
||||
export function doToggleSearchExpanded() {
|
||||
return {
|
||||
type: ACTIONS.TOGGLE_SEARCH_EXPANDED,
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
selectBalance,
|
||||
makeSelectChannelForClaimUri,
|
||||
parseURI,
|
||||
creditsToString,
|
||||
doError,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
|
@ -293,13 +292,7 @@ export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize:
|
|||
data: { uri, page },
|
||||
});
|
||||
|
||||
const { claimName, claimId } = parseURI(uri);
|
||||
let channelName = claimName;
|
||||
if (claimId) {
|
||||
channelName += `#${claimId}`;
|
||||
}
|
||||
|
||||
Lbry.claim_search({ channel_name: channelName, page, page_size: pageSize }).then(result => {
|
||||
Lbry.claim_search({ channel: uri, is_controlling: true, page, page_size: pageSize }).then(result => {
|
||||
const { items: claimsInChannel, page: returnedPage } = result;
|
||||
|
||||
if (claimsInChannel && claimsInChannel.length) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
|
|||
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
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 SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
|
||||
|
@ -35,8 +35,7 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
|
|||
Lbryio.call('subscription', 'list')
|
||||
.then(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)) {
|
||||
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 redux, but not in the db, add it to the db
|
||||
if (storedSubscriptions.length !== reduxSubscriptions.length) {
|
||||
const dbSubMap = {};
|
||||
const reduxSubMap = {};
|
||||
const subsNotInDB = [];
|
||||
const subscriptionsToReturn = reduxSubscriptions.slice();
|
||||
|
||||
storedSubscriptions.forEach(sub => {
|
||||
dbSubMap[sub.claim_id] = 1;
|
||||
});
|
||||
|
||||
reduxSubscriptions.forEach(sub => {
|
||||
const { claimId } = parseURI(sub.uri);
|
||||
reduxSubMap[claimId] = 1;
|
||||
|
||||
if (!dbSubMap[claimId]) {
|
||||
subsNotInDB.push({
|
||||
claim_id: claimId,
|
||||
channel_name: sub.channelName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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)))
|
||||
.then(() => subscriptionsToReturn)
|
||||
.catch(
|
||||
() =>
|
||||
// let it fail, we will try again when the navigate to the subscriptions page
|
||||
subscriptionsToReturn
|
||||
);
|
||||
return subscriptionsToReturn;
|
||||
}
|
||||
|
||||
// DB is already synced, just return the subscriptions in redux
|
||||
|
@ -223,85 +203,85 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
|||
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?
|
||||
Lbry.claim_search({ channel_id: claimId, page: 1, page_size: PAGE_SIZE }).then(claimListByChannel => {
|
||||
const { items: claimsInChannel } = claimListByChannel;
|
||||
Lbry.claim_search({ channel: subscriptionUri, is_controlling: true, page: 1, page_size: PAGE_SIZE }).then(
|
||||
claimListByChannel => {
|
||||
const { items: claimsInChannel } = claimListByChannel;
|
||||
|
||||
// may happen if subscribed to an abandoned channel or an empty channel
|
||||
if (!claimsInChannel || !claimsInChannel.length) {
|
||||
return;
|
||||
}
|
||||
// may happen if subscribed to an abandoned channel or an empty channel
|
||||
if (!claimsInChannel || !claimsInChannel.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if the latest subscription currently saved is actually the latest subscription
|
||||
const latestIndex = claimsInChannel.findIndex(
|
||||
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
|
||||
);
|
||||
// Determine if the latest subscription currently saved is actually the latest subscription
|
||||
const latestIndex = claimsInChannel.findIndex(
|
||||
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
|
||||
);
|
||||
|
||||
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
|
||||
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
|
||||
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
|
||||
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
|
||||
|
||||
// If latest is 0, nothing has changed
|
||||
// Do not download/notify about new content, it would download/notify 10 claims per channel
|
||||
if (latestIndex !== 0 && savedSubscription.latest) {
|
||||
let downloadCount = 0;
|
||||
// If latest is 0, nothing has changed
|
||||
// Do not download/notify about new content, it would download/notify 10 claims per channel
|
||||
if (latestIndex !== 0 && savedSubscription.latest) {
|
||||
let downloadCount = 0;
|
||||
|
||||
const newUnread = [];
|
||||
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
|
||||
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
|
||||
const shouldDownload =
|
||||
shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
|
||||
const newUnread = [];
|
||||
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
|
||||
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
|
||||
const shouldDownload =
|
||||
shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
|
||||
|
||||
// Add the new content to the list of "un-read" subscriptions
|
||||
if (shouldNotify) {
|
||||
newUnread.push(uri);
|
||||
}
|
||||
// Add the new content to the list of "un-read" subscriptions
|
||||
if (shouldNotify) {
|
||||
newUnread.push(uri);
|
||||
}
|
||||
|
||||
if (shouldDownload) {
|
||||
downloadCount += 1;
|
||||
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
|
||||
}
|
||||
});
|
||||
if (shouldDownload) {
|
||||
downloadCount += 1;
|
||||
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(
|
||||
doUpdateUnreadSubscriptions(
|
||||
subscriptionUri,
|
||||
newUnread,
|
||||
downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Set the latest piece of content for a channel
|
||||
// This allows the app to know if there has been new content since it was last set
|
||||
dispatch(
|
||||
doUpdateUnreadSubscriptions(
|
||||
subscriptionUri,
|
||||
newUnread,
|
||||
downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
|
||||
setSubscriptionLatest(
|
||||
{
|
||||
channelName: claimsInChannel[0].channel_name,
|
||||
uri: buildURI(
|
||||
{
|
||||
channelName: claimsInChannel[0].channel_name,
|
||||
claimId: claimsInChannel[0].claim_id,
|
||||
},
|
||||
false
|
||||
),
|
||||
},
|
||||
buildURI({ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Set the latest piece of content for a channel
|
||||
// This allows the app to know if there has been new content since it was last set
|
||||
dispatch(
|
||||
setSubscriptionLatest(
|
||||
{
|
||||
channelName: claimsInChannel[0].channel_name,
|
||||
uri: buildURI(
|
||||
{
|
||||
channelName: claimsInChannel[0].channel_name,
|
||||
claimId: claimsInChannel[0].claim_id,
|
||||
},
|
||||
false
|
||||
),
|
||||
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
|
||||
// means it will delete a non-existant fetchingChannelClaims[uri]
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
|
||||
data: {
|
||||
uri: subscriptionUri,
|
||||
claims: claimsInChannel || [],
|
||||
page: 1,
|
||||
},
|
||||
buildURI({ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false)
|
||||
)
|
||||
);
|
||||
|
||||
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
|
||||
// means it will delete a non-existant fetchingChannelClaims[uri]
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
|
||||
data: {
|
||||
uri: subscriptionUri,
|
||||
claims: claimsInChannel || [],
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
|
|
@ -2,14 +2,7 @@
|
|||
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
// @if TARGET='app'
|
||||
// $FlowFixMe
|
||||
import { remote } from 'electron';
|
||||
// @endif
|
||||
// @if TARGET='web'
|
||||
// $FlowFixMe
|
||||
import { remote } from 'web/stubs';
|
||||
// @endif
|
||||
|
||||
// @if TARGET='app'
|
||||
const win = remote.BrowserWindow.getFocusedWindow();
|
||||
|
@ -43,7 +36,6 @@ export type AppState = {
|
|||
isUpgradeAvailable: ?boolean,
|
||||
isUpgradeSkipped: ?boolean,
|
||||
hasClickedComment: boolean,
|
||||
enhancedLayout: boolean,
|
||||
searchOptionsExpanded: boolean,
|
||||
};
|
||||
|
||||
|
@ -228,11 +220,6 @@ reducers[ACTIONS.AUTHENTICATION_FAILURE] = state =>
|
|||
modal: MODALS.AUTHENTICATION_FAILURE,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.ENNNHHHAAANNNCEEE] = state =>
|
||||
Object.assign({}, state, {
|
||||
enhancedLayout: !state.enhancedLayout,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state =>
|
||||
Object.assign({}, state, {
|
||||
searchOptionsExpanded: !state.searchOptionsExpanded,
|
||||
|
|
|
@ -19,11 +19,9 @@ const defaultState = {
|
|||
[SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
|
||||
[SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_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.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
|
||||
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'dark'),
|
||||
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'light'),
|
||||
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
|
||||
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
|
||||
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
|
||||
|
|
|
@ -134,14 +134,6 @@ export default handleActions(
|
|||
...state,
|
||||
loadingSuggested: false,
|
||||
}),
|
||||
[ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED]: (state: SubscriptionState): SubscriptionState => ({
|
||||
...state,
|
||||
firstRunCompleted: true,
|
||||
}),
|
||||
[ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
|
||||
...state,
|
||||
showSuggestedSubs: true,
|
||||
}),
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { createSelector } from 'reselect';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
|
||||
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(
|
||||
selectState,
|
||||
state => state.searchOptionsExpanded
|
||||
);
|
||||
|
||||
export const selectShouldShowInviteGuide = createSelector(
|
||||
makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED),
|
||||
makeSelectClientSetting(SETTINGS.INVITE_ACKNOWLEDGED),
|
||||
(firstRunCompleted, inviteAcknowledged) => {
|
||||
return firstRunCompleted ? !inviteAcknowledged : false;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
parseURI,
|
||||
} from 'lbry-redux';
|
||||
import { swapKeyAndValue } from 'util/swap-json';
|
||||
import { shuffleArray } from 'util/shuffleArray';
|
||||
|
||||
// Returns the entire subscriptions state
|
||||
const selectState = state => state.subscriptions || {};
|
||||
|
@ -86,13 +85,10 @@ export const selectSuggestedChannels = createSelector(
|
|||
}
|
||||
});
|
||||
|
||||
return Object.keys(suggestedChannels)
|
||||
.map(uri => ({
|
||||
uri,
|
||||
label: suggestedChannels[uri],
|
||||
}))
|
||||
.sort(shuffleArray)
|
||||
.slice(0, 5);
|
||||
return Object.keys(suggestedChannels).map(uri => ({
|
||||
uri,
|
||||
label: suggestedChannels[uri],
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
@import 'init/gui';
|
||||
@import 'component/animation';
|
||||
@import 'component/badge';
|
||||
@import 'component/banner';
|
||||
@import 'component/button';
|
||||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
|
@ -19,6 +18,8 @@
|
|||
@import 'component/dat-gui';
|
||||
@import 'component/expandable';
|
||||
@import 'component/file-download';
|
||||
@import 'component/file-list';
|
||||
@import 'component/file-properties';
|
||||
@import 'component/file-render';
|
||||
@import 'component/form-field';
|
||||
@import 'component/header';
|
||||
|
@ -33,7 +34,6 @@
|
|||
@import 'component/notice';
|
||||
@import 'component/pagination';
|
||||
@import 'component/placeholder';
|
||||
@import 'component/scrollbar';
|
||||
@import 'component/search';
|
||||
@import 'component/snack-bar';
|
||||
@import 'component/spinner';
|
||||
|
@ -42,6 +42,7 @@
|
|||
@import 'component/syntax-highlighter';
|
||||
@import 'component/table';
|
||||
@import 'component/tabs';
|
||||
@import 'component/tags';
|
||||
@import 'component/time';
|
||||
@import 'component/toggle';
|
||||
@import 'component/tooltip';
|
||||
|
|
|
@ -1 +1,18 @@
|
|||
@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;
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
|
@ -3,16 +3,8 @@
|
|||
.button {
|
||||
display: inline-block;
|
||||
|
||||
.button__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
stroke-width: 1.9;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
position: relative;
|
||||
color: $lbry-gray-5;
|
||||
|
||||
|
@ -23,12 +15,6 @@
|
|||
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 {
|
||||
|
@ -56,16 +42,57 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--alt {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button--uri-indicator {
|
||||
max-width: 100%;
|
||||
height: 1.2em;
|
||||
vertical-align: text-top;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.2s;
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
.card {
|
||||
background-color: $lbry-white;
|
||||
margin-bottom: var(--spacing-vertical-xlarge);
|
||||
margin-bottom: var(--spacing-xlarge);
|
||||
position: relative;
|
||||
border-radius: var(--card-radius);
|
||||
box-shadow: var(--card-box-shadow) $lbry-gray-1;
|
||||
overflow: hidden;
|
||||
|
||||
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%);
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +22,11 @@
|
|||
}
|
||||
|
||||
.card--section {
|
||||
padding: var(--spacing-vertical-large);
|
||||
position: relative;
|
||||
padding: var(--spacing-large);
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.card--modal {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// C A R D
|
||||
// A C T I O N S
|
||||
|
||||
|
@ -47,7 +53,7 @@
|
|||
font-size: 1.15rem;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-vertical-medium);
|
||||
margin-right: var(--spacing-medium);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +80,7 @@
|
|||
}
|
||||
|
||||
.card__actions--top-space {
|
||||
padding-top: var(--spacing-vertical-small);
|
||||
padding-top: var(--spacing-small);
|
||||
}
|
||||
|
||||
// C A R D
|
||||
|
@ -84,13 +90,12 @@
|
|||
font-size: 1.25rem;
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-bottom: var(--spacing-vertical-medium);
|
||||
margin-bottom: var(--spacing-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
bottom: -0.15rem;
|
||||
position: relative;
|
||||
}
|
||||
.card__content--large {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
// C A R D
|
||||
|
@ -100,25 +105,17 @@
|
|||
position: relative;
|
||||
|
||||
&: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
|
||||
// L I S T
|
||||
|
||||
.card__list {
|
||||
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
|
||||
// each row change and are auto-sized
|
||||
|
@ -135,31 +132,21 @@
|
|||
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));
|
||||
}
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1050px) {
|
||||
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));
|
||||
}
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr));
|
||||
}
|
||||
|
||||
.card__list--rewards {
|
||||
column-count: 2;
|
||||
column-gap: var(--spacing-vertical-medium);
|
||||
margin-bottom: var(--spacing-vertical-large);
|
||||
column-gap: var(--spacing-medium);
|
||||
margin-bottom: var(--spacing-large);
|
||||
|
||||
.card {
|
||||
display: inline-block;
|
||||
margin: 0 0 var(--spacing-vertical-medium);
|
||||
margin: 0 0 var(--spacing-medium);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -169,8 +156,7 @@
|
|||
|
||||
.card__message {
|
||||
border-left: 0.5rem solid;
|
||||
padding: var(--spacing-vertical-medium) var(--spacing-vertical-medium) var(--spacing-vertical-medium)
|
||||
var(--spacing-vertical-large);
|
||||
padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-medium) var(--spacing-large);
|
||||
|
||||
&:not(&--error):not(&--failure):not(&--success) {
|
||||
background-color: rgba($lbry-teal-1, 0.1);
|
||||
|
@ -198,22 +184,24 @@
|
|||
|
||||
.card__subtitle {
|
||||
@extend .help;
|
||||
background-color: lighten($lbry-gray-1, 7%);
|
||||
color: darken($lbry-gray-5, 30%);
|
||||
color: darken($lbry-gray-5, 25%);
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: var(--spacing-vertical-small);
|
||||
margin-bottom: var(--spacing-small);
|
||||
flex: 1;
|
||||
|
||||
p {
|
||||
margin-bottom: var(--spacing-vertical-small);
|
||||
margin-bottom: var(--spacing-small);
|
||||
}
|
||||
|
||||
.badge {
|
||||
bottom: -0.12rem;
|
||||
position: relative;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
[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 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-vertical-medium);
|
||||
margin-bottom: var(--spacing-medium);
|
||||
|
||||
+ .card__content {
|
||||
margin-top: var(--spacing-vertical-medium);
|
||||
margin-top: var(--spacing-medium);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +223,7 @@
|
|||
align-items: center;
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: var(--spacing-vertical-medium);
|
||||
margin-right: var(--spacing-medium);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ $metadata-z-index: 1;
|
|||
align-items: flex-end;
|
||||
box-sizing: content-box;
|
||||
color: $lbry-white;
|
||||
border-top-left-radius: var(--card-radius);
|
||||
border-top-right-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
.channel-cover__custom {
|
||||
|
@ -24,13 +26,19 @@ $metadata-z-index: 1;
|
|||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: var(--spacing-main-padding);
|
||||
height: var(--channel-thumbnail-size);
|
||||
width: var(--channel-thumbnail-size);
|
||||
height: 5.3rem;
|
||||
width: 5.4rem;
|
||||
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;
|
||||
left: var(--spacing-medium);
|
||||
}
|
||||
|
||||
.channel-thumbnail__custom {
|
||||
|
@ -44,7 +52,7 @@ $metadata-z-index: 1;
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
align-self: flex-end;
|
||||
margin-bottom: -1px;
|
||||
// margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.channel-thumbnail,
|
||||
|
@ -70,14 +78,15 @@ $metadata-z-index: 1;
|
|||
z-index: $metadata-z-index;
|
||||
// Jump over the thumbnail photo because it is absolutely positioned
|
||||
// Then add normal page spacing, _then_ add the actual padding
|
||||
margin-left: calc(var(--channel-thumbnail-size) + var(--spacing-main-padding));
|
||||
padding-left: var(--spacing-vertical-large);
|
||||
padding-bottom: var(--spacing-vertical-medium);
|
||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-large));
|
||||
// padding-left: var(--spacing-large);
|
||||
padding-bottom: var(--spacing-medium);
|
||||
}
|
||||
|
||||
.channel__title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin-right: var(--spacing-large);
|
||||
}
|
||||
|
||||
.channel__url {
|
||||
|
@ -85,3 +94,8 @@ $metadata-z-index: 1;
|
|||
margin-top: -0.25rem;
|
||||
color: rgba($lbry-white, 0.75);
|
||||
}
|
||||
|
||||
// TODO: rename
|
||||
.channel__data {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 var(--spacing-vertical-large);
|
||||
padding: 0 var(--spacing-large);
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue