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",
|
"@exponent/electron-cookies": "^2.0.0",
|
||||||
"@hot-loader/react-dom": "16.8",
|
"@hot-loader/react-dom": "16.8",
|
||||||
"@lbry/color": "^1.0.2",
|
"@lbry/color": "^1.0.2",
|
||||||
"@lbry/components": "^2.7.0",
|
"@lbry/components": "^2.7.2",
|
||||||
"@reach/rect": "^0.2.1",
|
"@reach/rect": "^0.2.1",
|
||||||
"@reach/tabs": "^0.1.5",
|
"@reach/tabs": "^0.1.5",
|
||||||
"@types/three": "^0.93.1",
|
"@types/three": "^0.93.1",
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
"jsmediatags": "^3.8.1",
|
"jsmediatags": "^3.8.1",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#02f6918238110726c0b3b4248c61a84ac0b969e3",
|
"lbry-redux": "lbryio/lbry-redux#08ed1be3905896452536c92f17997bcde4533aea",
|
||||||
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
|
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
|
@ -153,6 +153,7 @@
|
||||||
"react-router": "^5.0.0",
|
"react-router": "^5.0.0",
|
||||||
"react-router-dom": "^5.0.0",
|
"react-router-dom": "^5.0.0",
|
||||||
"react-simplemde-editor": "^4.0.0",
|
"react-simplemde-editor": "^4.0.0",
|
||||||
|
"react-spring": "^8.0.20",
|
||||||
"react-toggle": "^4.0.2",
|
"react-toggle": "^4.0.2",
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"redux-persist": "^4.8.0",
|
"redux-persist": "^4.8.0",
|
||||||
|
@ -191,7 +192,7 @@
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.37.4",
|
"lbrynetDaemonVersion": "0.38.0rc6",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
|
|
|
@ -84,19 +84,6 @@ export default appState => {
|
||||||
window.loadURL(rendererURL + deepLinkingURI);
|
window.loadURL(rendererURL + deepLinkingURI);
|
||||||
setupBarMenu();
|
setupBarMenu();
|
||||||
|
|
||||||
// Windows back/forward mouse navigation
|
|
||||||
window.on('app-command', (e, cmd) => {
|
|
||||||
switch (cmd) {
|
|
||||||
case 'browser-backward':
|
|
||||||
window.webContents.send('navigate-backward', null);
|
|
||||||
break;
|
|
||||||
case 'browser-forward':
|
|
||||||
window.webContents.send('navigate-forward', null);
|
|
||||||
break;
|
|
||||||
default: // Do nothing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on('close', event => {
|
window.on('close', event => {
|
||||||
if (!appState.isQuitting && !appState.autoUpdateAccepted) {
|
if (!appState.isQuitting && !appState.autoUpdateAccepted) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
export const clipboard = () => {
|
export const clipboard = () => {
|
||||||
throw 'Fix me!';
|
throw new Error('Fix me!');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ipcRenderer = () => {
|
export const ipcRenderer = () => {
|
||||||
throw 'Fix me!';
|
throw new Error('Fix me!');
|
||||||
};
|
|
||||||
|
|
||||||
export const remote = () => {
|
|
||||||
throw 'Fix me!';
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const callable = () => {
|
const callable = () => {
|
||||||
throw Error('Need to fix this stub');
|
throw Error('Need to fix this stub');
|
||||||
};
|
};
|
||||||
const returningCallable = value => () => value;
|
|
||||||
|
|
||||||
export const remote = {
|
export const remote = {
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doUpdateBlockHeight, doError } from 'lbry-redux';
|
import { doUpdateBlockHeight, doError } from 'lbry-redux';
|
||||||
import { doToggleEnhancedLayout } from 'redux/actions/app';
|
import { selectUser, doRewardList, doFetchRewardedContent } from 'lbryinc';
|
||||||
import { selectUser } from 'lbryinc';
|
|
||||||
import { selectThemePath } from 'redux/selectors/settings';
|
import { selectThemePath } from 'redux/selectors/settings';
|
||||||
import { selectEnhancedLayout } from 'redux/selectors/app';
|
|
||||||
import App from './view';
|
import App from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
theme: selectThemePath(state),
|
theme: selectThemePath(state),
|
||||||
enhancedLayout: selectEnhancedLayout(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
alertError: errorList => dispatch(doError(errorList)),
|
alertError: errorList => dispatch(doError(errorList)),
|
||||||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||||
toggleEnhancedLayout: () => dispatch(doToggleEnhancedLayout()),
|
fetchRewards: () => dispatch(doRewardList()),
|
||||||
|
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default hot(
|
export default hot(
|
||||||
|
|
|
@ -1,82 +1,54 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import Router from 'component/router/index';
|
import Router from 'component/router/index';
|
||||||
import ModalRouter from 'modal/modalRouter';
|
import ModalRouter from 'modal/modalRouter';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import SideBar from 'component/sideBar';
|
import SideBar from 'component/sideBar';
|
||||||
import Header from 'component/header';
|
import Header from 'component/header';
|
||||||
import { openContextMenu } from 'util/context-menu';
|
import { openContextMenu } from 'util/context-menu';
|
||||||
import EnhancedLayoutListener from 'util/enhanced-layout';
|
import useKonamiListener from 'util/enhanced-layout';
|
||||||
import Yrbl from 'component/yrbl';
|
import Yrbl from 'component/yrbl';
|
||||||
|
|
||||||
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
alertError: (string | {}) => void,
|
alertError: (string | {}) => void,
|
||||||
pageTitle: ?string,
|
pageTitle: ?string,
|
||||||
language: string,
|
language: string,
|
||||||
theme: string,
|
theme: string,
|
||||||
updateBlockHeight: () => void,
|
fetchRewards: () => void,
|
||||||
toggleEnhancedLayout: () => void,
|
fetchRewardedContent: () => void,
|
||||||
enhancedLayout: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class App extends React.PureComponent<Props> {
|
function App(props: Props) {
|
||||||
componentDidMount() {
|
const { theme, fetchRewards, fetchRewardedContent } = props;
|
||||||
const { updateBlockHeight, toggleEnhancedLayout, alertError, theme } = this.props;
|
const appRef = useRef();
|
||||||
|
const isEnhancedLayout = useKonamiListener();
|
||||||
|
|
||||||
// TODO: create type for this object
|
useEffect(() => {
|
||||||
// it lives in jsonrpc.js
|
ReactModal.setAppElement(appRef.current);
|
||||||
document.addEventListener('unhandledError', (event: any) => {
|
fetchRewards();
|
||||||
alertError(event.detail);
|
fetchRewardedContent();
|
||||||
});
|
}, [fetchRewards, fetchRewardedContent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
document.documentElement.setAttribute('data-mode', theme);
|
document.documentElement.setAttribute('data-mode', theme);
|
||||||
|
}, [theme]);
|
||||||
ReactModal.setAppElement('#window'); // fuck this
|
|
||||||
|
|
||||||
this.enhance = new EnhancedLayoutListener(() => toggleEnhancedLayout());
|
|
||||||
|
|
||||||
updateBlockHeight();
|
|
||||||
setInterval(() => {
|
|
||||||
updateBlockHeight();
|
|
||||||
}, TWO_POINT_FIVE_MINUTES);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
const { theme: prevTheme } = prevProps;
|
|
||||||
const { theme } = this.props;
|
|
||||||
|
|
||||||
if (prevTheme !== theme) {
|
|
||||||
// $FlowFixMe
|
|
||||||
document.documentElement.setAttribute('data-mode', theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.enhance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
enhance: ?any;
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { enhancedLayout } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="window" onContextMenu={e => openContextMenu(e)}>
|
<div ref={appRef} onContextMenu={e => openContextMenu(e)}>
|
||||||
<Header />
|
<Header />
|
||||||
<SideBar />
|
|
||||||
|
|
||||||
<div className="main-wrapper">
|
<div className="main-wrapper">
|
||||||
|
<div className="main-wrapper-inner">
|
||||||
<Router />
|
<Router />
|
||||||
|
<SideBar />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModalRouter />
|
<ModalRouter />
|
||||||
{enhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -62,8 +62,8 @@ class Button extends React.PureComponent<Props> {
|
||||||
'button--primary': button === 'primary',
|
'button--primary': button === 'primary',
|
||||||
'button--secondary': button === 'secondary',
|
'button--secondary': button === 'secondary',
|
||||||
'button--alt': button === 'alt',
|
'button--alt': button === 'alt',
|
||||||
'button--danger': button === 'danger',
|
|
||||||
'button--inverse': button === 'inverse',
|
'button--inverse': button === 'inverse',
|
||||||
|
'button--close': button === 'close',
|
||||||
'button--disabled': disabled,
|
'button--disabled': disabled,
|
||||||
'button--link': button === 'link',
|
'button--link': button === 'link',
|
||||||
'button--constrict': constrict,
|
'button--constrict': constrict,
|
||||||
|
|
|
@ -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;
|
const showAbout = description || email || website;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className="card--section">
|
||||||
{!showAbout && <h2 className="empty">{__('Nothing here yet')}</h2>}
|
{!showAbout && <h2 className="main--empty empty">{__('Nothing here yet')}</h2>}
|
||||||
{showAbout && (
|
{showAbout && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{description && (
|
{description && (
|
||||||
|
|
|
@ -19,7 +19,6 @@ type Props = {
|
||||||
function ChannelContent(props: Props) {
|
function ChannelContent(props: Props) {
|
||||||
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props;
|
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props;
|
||||||
const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
|
const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{fetching && !hasContent && (
|
{fetching && !hasContent && (
|
||||||
|
@ -28,11 +27,15 @@ function ChannelContent(props: Props) {
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!fetching && !hasContent && <h2 className="empty">{__("This channel hasn't uploaded anything.")}</h2>}
|
{!fetching && !hasContent && (
|
||||||
|
<div className="card--section">
|
||||||
|
<h2 className="card__content help">{__("This channel hasn't uploaded anything.")}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
||||||
|
|
||||||
{hasContent && <FileList sortByHeight hideFilter fileInfos={claimsInChannel} />}
|
{hasContent && <FileList noHeader uris={claimsInChannel.map(claim => claim.permanent_url)} />}
|
||||||
|
|
||||||
<Paginate
|
<Paginate
|
||||||
onPageChange={page => fetchClaims(uri, page)}
|
onPageChange={page => fetchClaims(uri, page)}
|
||||||
|
|
|
@ -7,24 +7,25 @@ import Gerbil from './gerbil.png';
|
||||||
type Props = {
|
type Props = {
|
||||||
thumbnail: ?string,
|
thumbnail: ?string,
|
||||||
uri: string,
|
uri: string,
|
||||||
|
className?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelThumbnail(props: Props) {
|
function ChannelThumbnail(props: Props) {
|
||||||
const { thumbnail, uri } = props;
|
const { thumbnail, uri, className } = props;
|
||||||
|
|
||||||
// Generate a random color class based on the first letter of the channel name
|
// Generate a random color class based on the first letter of the channel name
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
const initializer = channelName.charCodeAt(0) - 65; // will be between 0 and 57
|
const initializer = channelName.charCodeAt(0) - 65; // will be between 0 and 57
|
||||||
const className = `channel-thumbnail__default--${initializer % 4}`;
|
const colorClassName = `channel-thumbnail__default--${initializer % 4}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('channel-thumbnail', {
|
className={classnames('channel-thumbnail', className, {
|
||||||
[className]: !thumbnail,
|
[colorClassName]: !thumbnail,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!thumbnail && <img className="channel-thumbnail__default" src={Gerbil} />}
|
{!thumbnail && <img className="channel-thumbnail__default" src={Gerbil} />}
|
||||||
{thumbnail && <img className="channel-thumbnail__custom" src={thumbnail} />}
|
{thumbnail && <img className={classnames('channel-thumbnail__custom', className)} src={thumbnail} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
showFullPrice: boolean,
|
||||||
showPlus: boolean,
|
showPlus: boolean,
|
||||||
isEstimate?: boolean,
|
isEstimate?: boolean,
|
||||||
large?: boolean,
|
|
||||||
showLBC?: boolean,
|
showLBC?: boolean,
|
||||||
fee?: boolean,
|
fee?: boolean,
|
||||||
badge?: boolean,
|
badge?: boolean,
|
||||||
|
@ -27,7 +26,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { amount, precision, showFullPrice, showFree, showPlus, large, isEstimate, fee, showLBC, badge } = this.props;
|
const { amount, precision, showFullPrice, showFree, showPlus, isEstimate, fee, showLBC, badge } = this.props;
|
||||||
|
|
||||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||||
const fullPrice = formatFullPrice(amount, 2);
|
const fullPrice = formatFullPrice(amount, 2);
|
||||||
|
@ -69,7 +68,6 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
badge,
|
badge,
|
||||||
'badge--cost': badge && amount > 0,
|
'badge--cost': badge && amount > 0,
|
||||||
'badge--free': badge && isFree,
|
'badge--free': badge && isFree,
|
||||||
'badge--large': large,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{amountText}
|
{amountText}
|
||||||
|
|
|
@ -12,10 +12,6 @@ const select = state => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => () => ({
|
const perform = dispatch => () => ({
|
||||||
completeFirstRun: () => {
|
|
||||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
|
||||||
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
|
|
||||||
},
|
|
||||||
acknowledgeEmail: () => {
|
acknowledgeEmail: () => {
|
||||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 { connect } from 'react-redux';
|
||||||
import { selectClaimsById, doSetFileListSort } from 'lbry-redux';
|
|
||||||
import FileList from './view';
|
import FileList from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({});
|
||||||
claimsById: selectClaimsById(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({});
|
||||||
setFileListSort: (page, value) => dispatch(doSetFileListSort(page, value)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -1,165 +1,70 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { buildURI, SORT_OPTIONS } from 'lbry-redux';
|
import classnames from 'classnames';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import FileListItem from 'component/fileListItem';
|
||||||
import FileCard from 'component/fileCard';
|
import Spinner from 'component/spinner';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import usePersistedState from 'util/use-persisted-state';
|
||||||
|
|
||||||
|
const SORT_NEW = 'new';
|
||||||
|
const SORT_OLD = 'old';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hideFilter: boolean,
|
uris: Array<string>,
|
||||||
sortByHeight?: boolean,
|
header: React.Node,
|
||||||
claimsById: Array<StreamClaim>,
|
headerAltControls: React.Node,
|
||||||
fileInfos: Array<FileListItem>,
|
injectedItem?: React.Node,
|
||||||
sortBy: string,
|
loading: boolean,
|
||||||
page?: string,
|
noHeader?: boolean,
|
||||||
setFileListSort: (?string, string) => void,
|
slim?: string,
|
||||||
|
empty?: string,
|
||||||
|
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
||||||
|
persistedStorageKey?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileList extends React.PureComponent<Props> {
|
export default function FileList(props: Props) {
|
||||||
static defaultProps = {
|
const { uris, header, headerAltControls, injectedItem, loading, persistedStorageKey, noHeader, slim, empty } = props;
|
||||||
hideFilter: false,
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey || 'file-list-global-sort', SORT_NEW);
|
||||||
sortBy: SORT_OPTIONS.DATE_NEW,
|
const sortedUris = uris && currentSort === SORT_OLD ? uris.reverse() : uris;
|
||||||
};
|
const hasUris = uris && !!uris.length;
|
||||||
|
|
||||||
constructor(props: Props) {
|
function handleSortChange() {
|
||||||
super(props);
|
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||||
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
|
|
||||||
|
|
||||||
this.sortFunctions = {
|
|
||||||
[SORT_OPTIONS.DATE_NEW]: fileInfos =>
|
|
||||||
this.props.sortByHeight
|
|
||||||
? fileInfos.sort((fileInfo1, fileInfo2) => {
|
|
||||||
if (fileInfo1.confirmations < 1) {
|
|
||||||
return -1;
|
|
||||||
} else if (fileInfo2.confirmations < 1) {
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
|
||||||
? this.props.claimsById[fileInfo1.claim_id].height
|
|
||||||
: 0;
|
|
||||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
|
||||||
? this.props.claimsById[fileInfo2.claim_id].height
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (height1 !== height2) {
|
|
||||||
// flipped because heigher block height is newer
|
|
||||||
return height2 - height1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInfo1.absolute_channel_position && fileInfo2.absolute_channel_position) {
|
|
||||||
return fileInfo1.absolute_channel_position - fileInfo2.absolute_channel_position;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
: [...fileInfos].reverse(),
|
|
||||||
[SORT_OPTIONS.DATE_OLD]: fileInfos =>
|
|
||||||
this.props.sortByHeight
|
|
||||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
|
||||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
|
||||||
? this.props.claimsById[fileInfo1.claim_id].height
|
|
||||||
: 999999;
|
|
||||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
|
||||||
? this.props.claimsById[fileInfo2.claim_id].height
|
|
||||||
: 999999;
|
|
||||||
if (height1 < height2) {
|
|
||||||
return -1;
|
|
||||||
} else if (height1 > height2) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
: fileInfos,
|
|
||||||
[SORT_OPTIONS.TITLE]: fileInfos =>
|
|
||||||
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
|
||||||
const getFileTitle = fileInfo => {
|
|
||||||
const { value, name, claim_name: claimName } = fileInfo;
|
|
||||||
if (value) {
|
|
||||||
return value.title || claimName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid claim
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
const title1 = getFileTitle(fileInfo1).toLowerCase();
|
|
||||||
const title2 = getFileTitle(fileInfo2).toLowerCase();
|
|
||||||
if (title1 < title2) {
|
|
||||||
return -1;
|
|
||||||
} else if (title1 > title2) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}),
|
|
||||||
[SORT_OPTIONS.FILENAME]: fileInfos =>
|
|
||||||
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
|
|
||||||
const fileName1Lower = fileName1.toLowerCase();
|
|
||||||
const fileName2Lower = fileName2.toLowerCase();
|
|
||||||
if (fileName1Lower < fileName2Lower) {
|
|
||||||
return -1;
|
|
||||||
} else if (fileName2Lower > fileName1Lower) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getChannelSignature = (fileInfo: { pending: boolean } & FileListItem) => {
|
|
||||||
if (fileInfo.pending) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileInfo.channel_claim_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSortChanged(event: SyntheticInputEvent<*>) {
|
|
||||||
this.props.setFileListSort(this.props.page, event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
sortFunctions: {};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fileInfos, hideFilter, sortBy } = this.props;
|
|
||||||
|
|
||||||
const content = [];
|
|
||||||
if (!fileInfos) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
|
||||||
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId, txid, nout, isNew } = fileInfo;
|
|
||||||
const uriParams = {};
|
|
||||||
|
|
||||||
// This is unfortunate
|
|
||||||
// https://github.com/lbryio/lbry/issues/1159
|
|
||||||
const name = claimName || claimNameDownloaded;
|
|
||||||
uriParams.contentName = name;
|
|
||||||
uriParams.claimId = claimId;
|
|
||||||
const uri = buildURI(uriParams);
|
|
||||||
const outpoint = `${txid}:${nout}`;
|
|
||||||
|
|
||||||
// See https://github.com/lbryio/lbry-desktop/issues/1327 for discussion around using outpoint as the key
|
|
||||||
content.push(<FileCard key={outpoint} uri={uri} isNew={isNew} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className={classnames('file-list')}>
|
||||||
{!hideFilter && (
|
{!noHeader && (
|
||||||
<Form>
|
<div className="file-list__header">
|
||||||
<FormField label={__('Sort by')} type="select" value={sortBy} onChange={this.handleSortChanged}>
|
{header || (
|
||||||
<option value={SORT_OPTIONS.DATE_NEW}>{__('Newest First')}</option>
|
<FormField
|
||||||
<option value={SORT_OPTIONS.DATE_OLD}>{__('Oldest First')}</option>
|
className="file-list__dropdown"
|
||||||
<option value={SORT_OPTIONS.TITLE}>{__('Title')}</option>
|
type="select"
|
||||||
|
name="file_sort"
|
||||||
|
value={currentSort}
|
||||||
|
onChange={handleSortChange}
|
||||||
|
>
|
||||||
|
<option value={SORT_NEW}>{__('Newest First')}</option>
|
||||||
|
<option value={SORT_OLD}>{__('Oldest First')}</option>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Form>
|
|
||||||
)}
|
)}
|
||||||
|
{loading && <Spinner light type="small" />}
|
||||||
<section className="media-group--list">
|
<div className="file-list__alt-controls">{headerAltControls}</div>
|
||||||
<div className="card__list">{content}</div>
|
</div>
|
||||||
</section>
|
)}
|
||||||
|
{hasUris && (
|
||||||
|
<ul>
|
||||||
|
{sortedUris.map((uri, index) => (
|
||||||
|
<React.Fragment key={uri}>
|
||||||
|
<FileListItem uri={uri} slim={slim} />
|
||||||
|
{index === 4 && injectedItem && <li className="file-list__item--injected">{injectedItem}</li>}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{!hasUris && !loading && (
|
||||||
|
<div className="main--empty">{empty || <h3 className="card__title">{__('No results')}</h3>}</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default FileList;
|
|
||||||
|
|
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 {
|
import {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectMetadataForUri,
|
|
||||||
makeSelectFileInfoForUri,
|
|
||||||
makeSelectIsUriResolving,
|
makeSelectIsUriResolving,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
makeSelectClaimIsPending,
|
makeSelectClaimIsPending,
|
||||||
|
@ -11,25 +9,15 @@ import {
|
||||||
makeSelectTitleForUri,
|
makeSelectTitleForUri,
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectRewardContentClaimIds } from 'lbryinc';
|
|
||||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
|
||||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
import FileListItem from './view';
|
||||||
import { doClearContentHistoryUri } from 'redux/actions/content';
|
|
||||||
import FileCard from './view';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
obscureNsfw: !selectShowNsfw(state),
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
|
||||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
position: makeSelectContentPositionForUri(props.uri)(state),
|
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
|
||||||
isNew: makeSelectIsNew(props.uri)(state),
|
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
title: makeSelectTitleForUri(props.uri)(state),
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
|
@ -37,10 +25,9 @@ const select = (state, props) => ({
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
perform
|
perform
|
||||||
)(FileCard);
|
)(FileListItem);
|
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,42 +4,29 @@ import * as React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import WunderBar from 'component/wunderbar';
|
import WunderBar from 'component/wunderbar';
|
||||||
import Icon from 'component/common/icon';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
autoUpdateDownloaded: boolean,
|
autoUpdateDownloaded: boolean,
|
||||||
balance: string,
|
balance: string,
|
||||||
isUpgradeAvailable: boolean,
|
isUpgradeAvailable: boolean,
|
||||||
roundedBalance: string,
|
roundedBalance: number,
|
||||||
isBackDisabled: boolean,
|
|
||||||
isForwardDisabled: boolean,
|
|
||||||
back: () => void,
|
|
||||||
forward: () => void,
|
|
||||||
downloadUpgradeRequested: any => void,
|
downloadUpgradeRequested: any => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = (props: Props) => {
|
const Header = (props: Props) => {
|
||||||
const {
|
const { autoUpdateDownloaded, downloadUpgradeRequested, isUpgradeAvailable, roundedBalance } = props;
|
||||||
autoUpdateDownloaded,
|
|
||||||
balance,
|
|
||||||
downloadUpgradeRequested,
|
|
||||||
isUpgradeAvailable,
|
|
||||||
roundedBalance,
|
|
||||||
back,
|
|
||||||
isBackDisabled,
|
|
||||||
forward,
|
|
||||||
isForwardDisabled,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
|
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
|
<div className="title-bar" />
|
||||||
|
<div className="header__contents">
|
||||||
<div className="header__navigation">
|
<div className="header__navigation">
|
||||||
<Button
|
<Button
|
||||||
className="header__navigation-item header__navigation-item--lbry"
|
className="header__navigation-item header__navigation-item--lbry"
|
||||||
label={__('LBRY')}
|
label={__('LBRY')}
|
||||||
iconRight={ICONS.LBRY}
|
icon={ICONS.LBRY}
|
||||||
navigate="/"
|
navigate="/"
|
||||||
/>
|
/>
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
|
@ -66,23 +53,19 @@ const Header = (props: Props) => {
|
||||||
<WunderBar />
|
<WunderBar />
|
||||||
|
|
||||||
<div className="header__navigation">
|
<div className="header__navigation">
|
||||||
<Button
|
|
||||||
className="header__navigation-item header__navigation-item--menu"
|
|
||||||
description={__('Menu')}
|
|
||||||
icon={ICONS.MENU}
|
|
||||||
iconSize={15}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="header__navigation-item header__navigation-item--right-action"
|
className="header__navigation-item header__navigation-item--right-action"
|
||||||
activeClass="header__navigation-item--active"
|
activeClass="header__navigation-item--active"
|
||||||
description={__('Your wallet')}
|
|
||||||
title={`Your balance is ${balance} LBRY Credits`}
|
|
||||||
label={
|
label={
|
||||||
|
roundedBalance > 0 ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{roundedBalance} <LbcSymbol />
|
{roundedBalance} <LbcSymbol />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
__('Account')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
icon={ICONS.ACCOUNT}
|
||||||
navigate="/$/account"
|
navigate="/$/account"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -92,12 +75,10 @@ const Header = (props: Props) => {
|
||||||
description={__('Publish content')}
|
description={__('Publish content')}
|
||||||
icon={ICONS.UPLOAD}
|
icon={ICONS.UPLOAD}
|
||||||
iconSize={24}
|
iconSize={24}
|
||||||
label={isUpgradeAvailable ? '' : __('Publish')}
|
|
||||||
navigate="/$/publish"
|
navigate="/$/publish"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
|
|
||||||
{showUpgradeButton && (
|
{showUpgradeButton && (
|
||||||
<Button
|
<Button
|
||||||
className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade"
|
className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade"
|
||||||
|
@ -108,6 +89,15 @@ const Header = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="header__navigation-item header__navigation-item--right-action"
|
||||||
|
activeClass="header__navigation-item--active"
|
||||||
|
icon={ICONS.SETTINGS}
|
||||||
|
iconSize={24}
|
||||||
|
navigate="/$/settings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default function NavigationHistoryRecent(props: Props) {
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<Button navigate="/$/history/all" button="link" label={__('See All Visited Links')} />
|
<Button navigate="/$/library/all" button="link" label={__('See All Visited Links')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
|
@ -9,7 +9,6 @@ const LOADER_TIMEOUT = 1000;
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.Node | Array<React.Node>,
|
children: React.Node | Array<React.Node>,
|
||||||
pageTitle: ?string,
|
pageTitle: ?string,
|
||||||
notContained: ?boolean, // No max-width, but keep the padding
|
|
||||||
loading: ?boolean,
|
loading: ?boolean,
|
||||||
className: ?string,
|
className: ?string,
|
||||||
};
|
};
|
||||||
|
@ -69,16 +68,11 @@ class Page extends React.PureComponent<Props, State> {
|
||||||
loaderTimeout: ?TimeoutID;
|
loaderTimeout: ?TimeoutID;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, notContained, loading, className } = this.props;
|
const { children, loading, className } = this.props;
|
||||||
const { showLoader } = this.state;
|
const { showLoader } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main className={classnames('main', className)}>
|
||||||
className={classnames('main', className, {
|
|
||||||
'main--contained': !notContained,
|
|
||||||
'main--not-contained': notContained,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{!loading && children}
|
{!loading && children}
|
||||||
{showLoader && (
|
{showLoader && (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FileTile from 'component/fileTile';
|
import FileList from 'component/fileList';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -51,15 +51,14 @@ export default class RecommendedContent extends React.PureComponent<Props> {
|
||||||
const { recommendedContent, isSearching } = this.props;
|
const { recommendedContent, isSearching } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="media-group--list-recommended">
|
<section className="card">
|
||||||
<span>Related</span>
|
<FileList
|
||||||
{recommendedContent &&
|
slim
|
||||||
recommendedContent.map(recommendedUri => (
|
loading={isSearching}
|
||||||
<FileTile hideNoResult size="small" key={recommendedUri} uri={recommendedUri} />
|
uris={recommendedContent}
|
||||||
))}
|
header={<span>Related</span>}
|
||||||
{recommendedContent && !recommendedContent.length && !isSearching && (
|
empty={<div className="empty">{__('No related content found')}</div>}
|
||||||
<div className="media__subtitle">No related content found</div>
|
/>
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,8 @@ class RewardSummary extends React.Component<Props> {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{__('There are no rewards available at this time, please check back later')}.
|
{__('There are no rewards available at this time, please check back later')}.
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}{' '}
|
||||||
|
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -55,11 +56,6 @@ class RewardSummary extends React.Component<Props> {
|
||||||
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
|
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="help">
|
|
||||||
{__('Read our')} <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/rewards" />{' '}
|
|
||||||
{__('to learn more about LBRY Rewards')}.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,6 +19,8 @@ import SearchPage from 'page/search';
|
||||||
import UserHistoryPage from 'page/userHistory';
|
import UserHistoryPage from 'page/userHistory';
|
||||||
import SendCreditsPage from 'page/sendCredits';
|
import SendCreditsPage from 'page/sendCredits';
|
||||||
import NavigationHistory from 'page/navigationHistory';
|
import NavigationHistory from 'page/navigationHistory';
|
||||||
|
import TagsPage from 'page/tags';
|
||||||
|
import TagsEditPage from 'page/tagsEdit';
|
||||||
|
|
||||||
const Scroll = withRouter(function ScrollWrapper(props) {
|
const Scroll = withRouter(function ScrollWrapper(props) {
|
||||||
const { pathname } = props.location;
|
const { pathname } = props.location;
|
||||||
|
@ -50,11 +52,12 @@ export default function AppRouter() {
|
||||||
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
||||||
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
|
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
|
||||||
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
|
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
|
||||||
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} />
|
<Route path={`/$/${PAGES.LIBRARY}`} exact component={UserHistoryPage} />
|
||||||
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
|
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
|
||||||
<Route path={`/$/${PAGES.SEND}`} exact component={SendCreditsPage} />
|
<Route path={`/$/${PAGES.SEND}`} exact component={SendCreditsPage} />
|
||||||
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} />
|
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
|
||||||
<Route path={`/$/${PAGES.HISTORY}/all`} exact component={NavigationHistory} />
|
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
|
||||||
|
<Route path={`/$/${PAGES.TAGS}/edit`} exact component={TagsEditPage} />
|
||||||
|
|
||||||
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
||||||
<Route path="/:claimName" exact component={ShowPage} />
|
<Route path="/:claimName" exact component={ShowPage} />
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions, doToast } from 'lbry-redux';
|
import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions } from 'lbry-redux';
|
||||||
import { doToggleSearchExpanded } from 'redux/actions/app';
|
import { doToggleSearchExpanded } from 'redux/actions/app';
|
||||||
import { selectSearchOptionsExpanded } from 'redux/selectors/app';
|
import { selectSearchOptionsExpanded } from 'redux/selectors/app';
|
||||||
import analytics from 'analytics';
|
|
||||||
import SearchOptions from './view';
|
import SearchOptions from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
|
@ -14,24 +13,6 @@ const select = state => ({
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
|
setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
|
||||||
toggleSearchExpanded: () => dispatch(doToggleSearchExpanded()),
|
toggleSearchExpanded: () => dispatch(doToggleSearchExpanded()),
|
||||||
onFeedbackPositive: query => {
|
|
||||||
analytics.apiSearchFeedback(query, 1);
|
|
||||||
dispatch(
|
|
||||||
doToast({
|
|
||||||
message: __('Thanks for the feedback! You help make the app better for everyone.'),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onFeedbackNegative: query => {
|
|
||||||
analytics.apiSearchFeedback(query, 0);
|
|
||||||
dispatch(
|
|
||||||
doToast({
|
|
||||||
message: __(
|
|
||||||
'Thanks for the feedback. Mark has been notified and is currently walking over to his computer to work on this.'
|
|
||||||
),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -16,39 +16,20 @@ type Props = {
|
||||||
options: {},
|
options: {},
|
||||||
expanded: boolean,
|
expanded: boolean,
|
||||||
toggleSearchExpanded: () => void,
|
toggleSearchExpanded: () => void,
|
||||||
query: string,
|
|
||||||
onFeedbackPositive: string => void,
|
|
||||||
onFeedbackNegative: string => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchOptions = (props: Props) => {
|
const SearchOptions = (props: Props) => {
|
||||||
const {
|
const { options, setSearchOption, expanded, toggleSearchExpanded } = props;
|
||||||
options,
|
|
||||||
setSearchOption,
|
|
||||||
expanded,
|
|
||||||
toggleSearchExpanded,
|
|
||||||
query,
|
|
||||||
onFeedbackPositive,
|
|
||||||
onFeedbackNegative,
|
|
||||||
} = props;
|
|
||||||
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
|
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="search__options-wrapper">
|
<div>
|
||||||
<div className="card--space-between">
|
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
label={__('FILTER')}
|
label={__('FILTER')}
|
||||||
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
|
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
|
||||||
onClick={toggleSearchExpanded}
|
onClick={toggleSearchExpanded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="media__action-group">
|
|
||||||
<span>{__('Find what you were looking for?')}</span>
|
|
||||||
<Button button="alt" description={__('Yes')} onClick={() => onFeedbackPositive(query)} icon={ICONS.YES} />
|
|
||||||
<Button button="alt" description={__('No')} onClick={() => onFeedbackNegative(query)} icon={ICONS.NO} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ExpandableOptions pose={expanded ? 'show' : 'hide'}>
|
<ExpandableOptions pose={expanded ? 'show' : 'hide'}>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<Form className="card__content search__options">
|
<Form className="card__content search__options">
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectUnreadAmount } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { selectShouldShowInviteGuide } from 'redux/selectors/app';
|
import { selectFollowedTags } from 'lbry-redux';
|
||||||
import SideBar from './view';
|
import SideBar from './view';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { SETTINGS } from 'lbry-redux';
|
import { SETTINGS } from 'lbry-redux';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
unreadSubscriptionTotal: selectUnreadAmount(state),
|
subscriptions: selectSubscriptions(state),
|
||||||
|
followedTags: selectFollowedTags(state),
|
||||||
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
||||||
shouldShowInviteGuide: selectShouldShowInviteGuide(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = () => ({});
|
const perform = () => ({});
|
||||||
|
|
|
@ -3,17 +3,15 @@ import * as PAGES from 'constants/pages';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import Tag from 'component/tag';
|
||||||
import Tooltip from 'component/common/tooltip';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
unreadSubscriptionTotal: number,
|
subscriptions: Array<Subscription>,
|
||||||
shouldShowInviteGuide: string,
|
followedTags: Array<Tag>,
|
||||||
};
|
};
|
||||||
|
|
||||||
class SideBar extends React.PureComponent<Props> {
|
function SideBar(props: Props) {
|
||||||
render() {
|
const { subscriptions, followedTags } = props;
|
||||||
const { unreadSubscriptionTotal, shouldShowInviteGuide } = this.props;
|
|
||||||
const buildLink = (path, label, icon, guide) => ({
|
const buildLink = (path, label, icon, guide) => ({
|
||||||
navigate: path ? `$/${path}` : '/',
|
navigate: path ? `$/${path}` : '/',
|
||||||
label,
|
label,
|
||||||
|
@ -21,89 +19,58 @@ class SideBar extends React.PureComponent<Props> {
|
||||||
guide,
|
guide,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderLink = (linkProps, index) => {
|
const renderLink = linkProps => (
|
||||||
const { guide } = linkProps;
|
<Button {...linkProps} key={linkProps.label} className="navigation__link" activeClass="navigation__link--active" />
|
||||||
|
|
||||||
const inner = (
|
|
||||||
<Button
|
|
||||||
{...linkProps}
|
|
||||||
className={classnames('navigation__link', {
|
|
||||||
'navigation__link--guide': guide,
|
|
||||||
})}
|
|
||||||
activeClass="navigation__link--active"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={index}>
|
<div className="navigation-wrapper">
|
||||||
{guide ? (
|
|
||||||
<Tooltip key={guide} alwaysVisible direction="right" body={guide}>
|
|
||||||
{inner}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
inner
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="navigation">
|
<nav className="navigation">
|
||||||
<ul className="navigation__links">
|
<ul className="navigation__links">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
...buildLink(null, __('Discover'), ICONS.DISCOVER),
|
...buildLink(null, __('Home'), ICONS.HOME),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...buildLink(
|
...buildLink(PAGES.SUBSCRIPTIONS, __('Subscriptions'), ICONS.SUBSCRIPTION),
|
||||||
PAGES.SUBSCRIPTIONS,
|
|
||||||
`${__('Subscriptions')} ${unreadSubscriptionTotal > 0 ? '(' + unreadSubscriptionTotal + ')' : ''}`,
|
|
||||||
ICONS.SUBSCRIPTION
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...buildLink(PAGES.HISTORY, __('Library'), ICONS.DOWNLOAD),
|
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.DOWNLOAD),
|
||||||
},
|
|
||||||
].map(renderLink)}
|
|
||||||
</ul>
|
|
||||||
<div className="navigation__link navigation__link--title">Account</div>
|
|
||||||
|
|
||||||
<ul className="navigation__links">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
...buildLink(PAGES.ACCOUNT, __('Overview'), ICONS.ACCOUNT),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...buildLink(PAGES.INVITE, __('Invite'), ICONS.INVITE, shouldShowInviteGuide && __('Check this out!')),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...buildLink(PAGES.REWARDS, __('Rewards'), ICONS.FEATURED),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...buildLink(PAGES.SEND, __('Send & Recieve'), ICONS.SEND),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...buildLink(PAGES.TRANSACTIONS, __('Transactions'), ICONS.TRANSACTIONS),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...buildLink(PAGES.SETTINGS, __('Settings'), ICONS.SETTINGS),
|
|
||||||
},
|
},
|
||||||
].map(renderLink)}
|
].map(renderLink)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul className="navigation__links navigation__links--bottom">
|
<Button
|
||||||
{[
|
navigate="/$/tags/edit"
|
||||||
{
|
iconRight={ICONS.SETTINGS}
|
||||||
...buildLink(PAGES.HELP, __('Help'), ICONS.HELP),
|
className="navigation__link--title navigation__link"
|
||||||
},
|
activeClass="navigation__link--active"
|
||||||
].map(renderLink)}
|
label={__('Following')}
|
||||||
|
/>
|
||||||
|
<ul className="tags--vertical navigation__links">
|
||||||
|
{followedTags.map(({ name }, key) => (
|
||||||
|
<li key={name}>
|
||||||
|
<Tag navigate={`/$/tags?t${name}`} name={name} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<ul className="navigation__links--small">
|
||||||
|
{subscriptions.map(({ uri, channelName }) => (
|
||||||
|
<Button
|
||||||
|
key={uri}
|
||||||
|
navigate={uri}
|
||||||
|
label={channelName}
|
||||||
|
className="navigation__link"
|
||||||
|
activeClass="navigation__link--active"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default SideBar;
|
export default SideBar;
|
||||||
|
|
|
@ -66,8 +66,8 @@ class Spinner extends PureComponent<Props, State> {
|
||||||
className={classnames('spinner', {
|
className={classnames('spinner', {
|
||||||
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
|
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
|
||||||
'spinner--light': !dark && (light || theme === DARK_THEME),
|
'spinner--light': !dark && (light || theme === DARK_THEME),
|
||||||
'spinner--splash': type === 'splash',
|
|
||||||
'spinner--small': type === 'small',
|
'spinner--small': type === 'small',
|
||||||
|
'spinner--splash': type === 'splash',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="rect rect1" />
|
<div className="rect rect1" />
|
||||||
|
|
|
@ -22,7 +22,7 @@ type Props = {
|
||||||
buttonStyle: string,
|
buttonStyle: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (props: Props) => {
|
export default function SubscribeButton(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
uri,
|
||||||
doChannelSubscribe,
|
doChannelSubscribe,
|
||||||
|
@ -36,14 +36,14 @@ export default (props: Props) => {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
const subscriptionLabel = isSubscribed ? __('Subscribed') : __('Subscribe');
|
||||||
|
|
||||||
const { claimName } = parseURI(uri);
|
const { claimName } = parseURI(uri);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
iconColor="red"
|
iconColor="red"
|
||||||
icon={isSubscribed ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIPTION}
|
icon={ICONS.SUBSCRIPTION}
|
||||||
button={buttonStyle || 'alt'}
|
button={buttonStyle || 'alt'}
|
||||||
label={subscriptionLabel}
|
label={subscriptionLabel}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
@ -64,4 +64,4 @@ export default (props: Props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -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 && (
|
{!fetchingTransactions && !hasTransactions && (
|
||||||
<div className="card__content">
|
<div className="card__content">
|
||||||
<p className="card__subtitle">{__('No transactions... yet.')}</p>
|
<p className="card__subtitle">{__('No transactions.')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,7 @@ type Props = {
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
channelUri: ?string,
|
channelUri: ?string,
|
||||||
link: ?boolean,
|
link: ?boolean,
|
||||||
claim: ?StreamClaim,
|
claim: ?Claim,
|
||||||
channelClaim: ?ChannelClaim,
|
|
||||||
// Lint thinks we aren't using these, even though we are.
|
// Lint thinks we aren't using these, even though we are.
|
||||||
// Possibly because the resolve function is an arrow function that is passed in props?
|
// Possibly because the resolve function is an arrow function that is passed in props?
|
||||||
resolveUri: string => void,
|
resolveUri: string => void,
|
||||||
|
@ -16,12 +15,12 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
class UriIndicator extends React.PureComponent<Props> {
|
class UriIndicator extends React.PureComponent<Props> {
|
||||||
componentWillMount() {
|
componentDidMount() {
|
||||||
this.resolve(this.props);
|
this.resolve(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: Props) {
|
componentDidUpdate() {
|
||||||
this.resolve(nextProps);
|
this.resolve(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve = (props: Props) => {
|
resolve = (props: Props) => {
|
||||||
|
@ -39,11 +38,16 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
|
return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!claim.signing_channel) {
|
const isChannelClaim = claim.value_type === 'channel';
|
||||||
|
|
||||||
|
if (!claim.signing_channel && !isChannelClaim) {
|
||||||
return <span className="channel-name">Anonymous</span>;
|
return <span className="channel-name">Anonymous</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, claim_id: claimId } = claim.signing_channel;
|
const channelClaim = isChannelClaim ? claim : claim.signing_channel;
|
||||||
|
|
||||||
|
if (channelClaim) {
|
||||||
|
const { name, claim_id: claimId } = channelClaim;
|
||||||
let channelLink;
|
let channelLink;
|
||||||
if (claim.is_channel_signature_valid) {
|
if (claim.is_channel_signature_valid) {
|
||||||
channelLink = link ? buildURI({ channelName: name, claimId }) : false;
|
channelLink = link ? buildURI({ channelName: name, claimId }) : false;
|
||||||
|
@ -60,6 +64,9 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
{inner}
|
{inner}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,6 @@ class UserEmailNew extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
<div className="card__actions">{cancelButton}</div>
|
<div className="card__actions">{cancelButton}</div>
|
||||||
<p className="help">{__('Your email address will never be sold and you can unsubscribe at any time.')}</p>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,11 @@ const WalletBalance = (props: Props) => {
|
||||||
</header>
|
</header>
|
||||||
<div className="card__content">
|
<div className="card__content">
|
||||||
<h3>{__('You currently have')}</h3>
|
<h3>{__('You currently have')}</h3>
|
||||||
{(balance || balance === 0) && <CreditAmount large badge={false} amount={balance} precision={8} />}
|
{(balance || balance === 0) && (
|
||||||
|
<span className="card__content--large">
|
||||||
|
<CreditAmount badge={false} amount={balance} precision={8} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,6 @@ export const SHOW_MODAL = 'SHOW_MODAL';
|
||||||
export const HIDE_MODAL = 'HIDE_MODAL';
|
export const HIDE_MODAL = 'HIDE_MODAL';
|
||||||
export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED';
|
export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED';
|
||||||
export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED';
|
export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED';
|
||||||
export const ENNNHHHAAANNNCEEE = 'ENNNHHHAAANNNCEEE';
|
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';
|
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';
|
||||||
|
@ -193,8 +192,6 @@ export const SET_VIEW_MODE = 'SET_VIEW_MODE';
|
||||||
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
|
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
|
||||||
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
|
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
|
||||||
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
|
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
|
||||||
export const SUBSCRIPTION_FIRST_RUN_COMPLETED = 'SUBSCRIPTION_FIRST_RUN_COMPLETED';
|
|
||||||
export const VIEW_SUGGESTED_SUBSCRIPTIONS = 'VIEW_SUGGESTED_SUBSCRIPTIONS';
|
|
||||||
|
|
||||||
// Publishing
|
// Publishing
|
||||||
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const DOWNLOAD = 'Download';
|
||||||
export const UPLOAD = 'UploadCloud';
|
export const UPLOAD = 'UploadCloud';
|
||||||
export const PUBLISHED = 'Cloud';
|
export const PUBLISHED = 'Cloud';
|
||||||
export const CLOSE = 'X';
|
export const CLOSE = 'X';
|
||||||
|
export const ADD = 'Plus';
|
||||||
export const EDIT = 'Edit3';
|
export const EDIT = 'Edit3';
|
||||||
export const DELETE = 'Trash';
|
export const DELETE = 'Trash';
|
||||||
export const REPORT = 'Flag';
|
export const REPORT = 'Flag';
|
||||||
|
|
|
@ -4,7 +4,7 @@ export const CHANNEL = 'channel';
|
||||||
export const DISCOVER = 'discover';
|
export const DISCOVER = 'discover';
|
||||||
export const DOWNLOADED = 'downloaded';
|
export const DOWNLOADED = 'downloaded';
|
||||||
export const HELP = 'help';
|
export const HELP = 'help';
|
||||||
export const HISTORY = 'history';
|
export const LIBRARY = 'library';
|
||||||
export const INVITE = 'invite';
|
export const INVITE = 'invite';
|
||||||
export const PUBLISH = 'publish';
|
export const PUBLISH = 'publish';
|
||||||
export const PUBLISHED = 'published';
|
export const PUBLISHED = 'published';
|
||||||
|
@ -18,3 +18,4 @@ export const ACCOUNT = 'account';
|
||||||
export const SUBSCRIPTIONS = 'subscriptions';
|
export const SUBSCRIPTIONS = 'subscriptions';
|
||||||
export const SEARCH = 'search';
|
export const SEARCH = 'search';
|
||||||
export const TRANSACTIONS = 'transactions';
|
export const TRANSACTIONS = 'transactions';
|
||||||
|
export const TAGS = 'tags';
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
|
export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
|
||||||
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
|
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
|
||||||
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
|
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
|
||||||
export const FIRST_RUN_COMPLETED = 'first_run_completed';
|
|
||||||
export const INVITE_ACKNOWLEDGED = 'invite_acknowledged';
|
export const INVITE_ACKNOWLEDGED = 'invite_acknowledged';
|
||||||
export const LANGUAGE = 'language';
|
export const LANGUAGE = 'language';
|
||||||
export const SHOW_NSFW = 'showNsfw';
|
export const SHOW_NSFW = 'showNsfw';
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import app from 'app';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
type ModalProps = {
|
type ModalProps = {
|
||||||
|
@ -20,7 +19,6 @@ type ModalProps = {
|
||||||
extraContent?: React.Node,
|
extraContent?: React.Node,
|
||||||
expandButtonLabel?: string,
|
expandButtonLabel?: string,
|
||||||
hideButtonLabel?: string,
|
hideButtonLabel?: string,
|
||||||
fullScreen: boolean,
|
|
||||||
title?: string | React.Node,
|
title?: string | React.Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +30,6 @@ export class Modal extends React.PureComponent<ModalProps> {
|
||||||
abortButtonLabel: __('Cancel'),
|
abortButtonLabel: __('Cancel'),
|
||||||
confirmButtonDisabled: false,
|
confirmButtonDisabled: false,
|
||||||
abortButtonDisabled: false,
|
abortButtonDisabled: false,
|
||||||
fullScreen: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -45,7 +42,6 @@ export class Modal extends React.PureComponent<ModalProps> {
|
||||||
abortButtonLabel,
|
abortButtonLabel,
|
||||||
abortButtonDisabled,
|
abortButtonDisabled,
|
||||||
onAborted,
|
onAborted,
|
||||||
fullScreen,
|
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
...modalProps
|
...modalProps
|
||||||
|
@ -54,10 +50,7 @@ export class Modal extends React.PureComponent<ModalProps> {
|
||||||
<ReactModal
|
<ReactModal
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
onRequestClose={onAborted || onConfirmed}
|
onRequestClose={onAborted || onConfirmed}
|
||||||
className={classnames('card', className, {
|
className={classnames('card card--modal modal', className)}
|
||||||
modal: !fullScreen,
|
|
||||||
'modal--fullscreen': fullScreen,
|
|
||||||
})}
|
|
||||||
overlayClassName="modal-overlay"
|
overlayClassName="modal-overlay"
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
|
|
|
@ -40,7 +40,7 @@ class ModalAffirmPurchase extends React.PureComponent<Props> {
|
||||||
onAborted={cancelPurchase}
|
onAborted={cancelPurchase}
|
||||||
>
|
>
|
||||||
<section className="card__content">
|
<section className="card__content">
|
||||||
<p>
|
<p className="card__subtitle">
|
||||||
{__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '}
|
{__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '}
|
||||||
<strong>
|
<strong>
|
||||||
<FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} />
|
<FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} />
|
||||||
|
|
|
@ -50,8 +50,6 @@ function ModalAutoGenerateThumbnail(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('resized');
|
|
||||||
|
|
||||||
const fixedWidth = 450;
|
const fixedWidth = 450;
|
||||||
const videoWidth = player.videoWidth;
|
const videoWidth = player.videoWidth;
|
||||||
const videoHeight = player.videoHeight;
|
const videoHeight = player.videoHeight;
|
||||||
|
|
|
@ -16,6 +16,8 @@ const WalletPage = () => (
|
||||||
<WalletBalance />
|
<WalletBalance />
|
||||||
<RewardSummary />
|
<RewardSummary />
|
||||||
</div>
|
</div>
|
||||||
|
<WalletAddress />
|
||||||
|
<WalletSend />
|
||||||
<TransactionListRecent />
|
<TransactionListRecent />
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -49,12 +49,13 @@ function ChannelPage(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page notContained className="main--no-padding-top">
|
<Page>
|
||||||
<header className="channel-cover main__item--extend-outside">
|
<div className="card">
|
||||||
|
<header className="channel-cover">
|
||||||
{cover && <img className="channel-cover__custom" src={cover} />}
|
{cover && <img className="channel-cover__custom" src={cover} />}
|
||||||
|
|
||||||
<div className="channel__primary-info">
|
<div className="channel__primary-info">
|
||||||
<ChannelThumbnail uri={uri} />
|
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="channel__title">{title || channelName}</h1>
|
<h1 className="channel__title">{title || channelName}</h1>
|
||||||
|
@ -67,7 +68,7 @@ function ChannelPage(props: Props) {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||||
<TabList className="main__item--extend-outside tabs__list--channel-page">
|
<TabList className="tabs__list--channel-page">
|
||||||
<Tab>{__('Content')}</Tab>
|
<Tab>{__('Content')}</Tab>
|
||||||
<Tab>{__('About')}</Tab>
|
<Tab>{__('About')}</Tab>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
|
@ -76,7 +77,7 @@ function ChannelPage(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels className="channel__data">
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<ChannelContent uri={uri} />
|
<ChannelContent uri={uri} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -85,6 +86,7 @@ function ChannelPage(props: Props) {
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,12 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import { selectFollowedTags } from 'lbry-redux';
|
||||||
doFetchRewardedContent,
|
|
||||||
doRewardList,
|
|
||||||
selectFeaturedUris,
|
|
||||||
doFetchFeaturedUris,
|
|
||||||
selectFetchingFeaturedUris,
|
|
||||||
} from 'lbryinc';
|
|
||||||
import DiscoverPage from './view';
|
import DiscoverPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
featuredUris: selectFeaturedUris(state),
|
followedTags: selectFollowedTags(state),
|
||||||
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = {};
|
||||||
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
|
|
||||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
|
||||||
fetchRewards: () => dispatch(doRewardList()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -1,82 +1,24 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import FileListDiscover from 'component/fileListDiscover';
|
||||||
|
import TagsSelect from 'component/tagsSelect';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import CategoryList from 'component/categoryList';
|
|
||||||
import FirstRun from 'component/firstRun';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fetchFeaturedUris: () => void,
|
followedTags: Array<Tag>,
|
||||||
fetchRewardedContent: () => void,
|
|
||||||
fetchRewards: () => void,
|
|
||||||
fetchingFeaturedUris: boolean,
|
|
||||||
featuredUris: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class DiscoverPage extends React.PureComponent<Props> {
|
function DiscoverPage(props: Props) {
|
||||||
constructor() {
|
const { followedTags } = props;
|
||||||
super();
|
|
||||||
this.continousFetch = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { fetchFeaturedUris, fetchRewardedContent, fetchRewards } = this.props;
|
|
||||||
fetchFeaturedUris();
|
|
||||||
fetchRewardedContent();
|
|
||||||
|
|
||||||
this.continousFetch = setInterval(() => {
|
|
||||||
fetchFeaturedUris();
|
|
||||||
fetchRewardedContent();
|
|
||||||
fetchRewards();
|
|
||||||
}, 1000 * 60 * 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearContinuousFetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategoryLinkPartByCategory(category: string) {
|
|
||||||
const channelName = category.substr(category.indexOf('@'));
|
|
||||||
if (!channelName.includes('#')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return channelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
trimClaimIdFromCategory(category: string) {
|
|
||||||
return category.split('#')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
continousFetch: ?IntervalID;
|
|
||||||
|
|
||||||
clearContinuousFetch() {
|
|
||||||
if (this.continousFetch) {
|
|
||||||
clearInterval(this.continousFetch);
|
|
||||||
this.continousFetch = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { featuredUris, fetchingFeaturedUris } = this.props;
|
|
||||||
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
|
|
||||||
const failedToLoad = !fetchingFeaturedUris && !hasContent;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page notContained isLoading={!hasContent && fetchingFeaturedUris} className="main--no-padding">
|
<Page className="card">
|
||||||
<FirstRun />
|
<FileListDiscover
|
||||||
{hasContent &&
|
personal
|
||||||
Object.keys(featuredUris).map(category => (
|
tags={followedTags.map(tag => tag.name)}
|
||||||
<CategoryList
|
injectedItem={<TagsSelect showClose title={__('Make This Your Own')} />}
|
||||||
lazyLoad
|
|
||||||
key={category}
|
|
||||||
category={this.trimClaimIdFromCategory(category)}
|
|
||||||
uris={featuredUris[category]}
|
|
||||||
categoryLink={this.getCategoryLinkPartByCategory(category)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default DiscoverPage;
|
export default DiscoverPage;
|
||||||
|
|
|
@ -108,15 +108,15 @@ class FilePage extends React.Component<Props> {
|
||||||
fetchViewCount(claim.claim_id);
|
fetchViewCount(claim.claim_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.uri !== uri) {
|
||||||
|
setViewed(uri);
|
||||||
|
}
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
if (fileInfo === undefined) {
|
if (fileInfo === undefined) {
|
||||||
fetchFileInfo(uri);
|
fetchFileInfo(uri);
|
||||||
}
|
}
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
if (prevProps.uri !== uri) {
|
|
||||||
setViewed(uri);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromSubscriptionNotifications() {
|
removeFromSubscriptionNotifications() {
|
||||||
|
@ -148,7 +148,8 @@ class FilePage extends React.Component<Props> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// File info
|
// File info
|
||||||
const { channel_name: channelName } = claim;
|
const { signing_channel: signingChannel } = claim;
|
||||||
|
const channelName = signingChannel && signingChannel.name;
|
||||||
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
|
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
|
||||||
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
|
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
|
||||||
const shouldObscureThumbnail = obscureNsfw && nsfw;
|
const shouldObscureThumbnail = obscureNsfw && nsfw;
|
||||||
|
@ -179,23 +180,12 @@ class FilePage extends React.Component<Props> {
|
||||||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page notContained className="main--file-page">
|
<Page className="main--file-page">
|
||||||
<div className="grid-area--content">
|
<div className="grid-area--content">
|
||||||
<Button
|
|
||||||
className="media__uri"
|
|
||||||
button="alt"
|
|
||||||
label={uri}
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.writeText(uri);
|
|
||||||
showToast({
|
|
||||||
message: __('Text copied'),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!fileInfo && insufficientCredits && (
|
{!fileInfo && insufficientCredits && (
|
||||||
<div className="media__insufficient-credits help--warning">
|
<div className="media__insufficient-credits help--warning">
|
||||||
{__(
|
{__(
|
||||||
'The publisher has chosen to charge LBC to view this content. Your balance is currently to low to view it.'
|
'The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it.'
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{__('Checkout')} <Button button="link" navigate="/$/rewards" label={__('the rewards page')} />{' '}
|
{__('Checkout')} <Button button="link" navigate="/$/rewards" label={__('the rewards page')} />{' '}
|
||||||
{__('or send more LBC to your wallet.')}
|
{__('or send more LBC to your wallet.')}
|
||||||
|
@ -223,12 +213,23 @@ class FilePage extends React.Component<Props> {
|
||||||
<div className="card__media-text">{__("Sorry, looks like we can't preview this file.")}</div>
|
<div className="card__media-text">{__("Sorry, looks like we can't preview this file.")}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<Button
|
||||||
|
className="media__uri"
|
||||||
|
button="alt"
|
||||||
|
label={uri}
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.writeText(uri);
|
||||||
|
showToast({
|
||||||
|
message: __('Copied'),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid-area--info media__content media__content--large">
|
<div className="grid-area--info media__content media__content--large">
|
||||||
<h1 className="media__title media__title--large">{title}</h1>
|
<h1 className="media__title media__title--large">{title}</h1>
|
||||||
|
|
||||||
<div className="media__properties media__properties--large">
|
<div className="file-properties">
|
||||||
{isRewardContent && (
|
{isRewardContent && (
|
||||||
<Icon
|
<Icon
|
||||||
size={20}
|
size={20}
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import { selectDownloadedUris, selectIsFetchingFileList } from 'lbry-redux';
|
||||||
selectFileInfosDownloaded,
|
|
||||||
selectMyClaimsWithoutChannels,
|
|
||||||
selectIsFetchingFileList,
|
|
||||||
selectFileListDownloadedSort,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import FileListDownloaded from './view';
|
import FileListDownloaded from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
fileInfos: selectFileInfosDownloaded(state),
|
downloadedUris: selectDownloadedUris(state),
|
||||||
fetching: selectIsFetchingFileList(state),
|
fetching: selectIsFetchingFileList(state),
|
||||||
claims: selectMyClaimsWithoutChannels(state),
|
|
||||||
sortBy: selectFileListDownloadedSort(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -2,26 +2,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileList from 'component/fileList';
|
import FileList from 'component/fileList';
|
||||||
import Page from 'component/page';
|
|
||||||
import { PAGES } from 'lbry-redux';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fetching: boolean,
|
fetching: boolean,
|
||||||
fileInfos: {},
|
downloadedUris: Array<string>,
|
||||||
sortBy: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileListDownloaded extends React.PureComponent<Props> {
|
function FileListDownloaded(props: Props) {
|
||||||
render() {
|
const { fetching, downloadedUris } = props;
|
||||||
const { fetching, fileInfos, sortBy } = this.props;
|
const hasDownloads = !!downloadedUris.length;
|
||||||
const hasDownloads = fileInfos && Object.values(fileInfos).length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Removed the <Page> wapper to try combining this page with UserHistory
|
// Removed the <Page> wapper to try combining this page with UserHistory
|
||||||
// This should eventually move into /components if we want to keep it this way
|
// This should eventually move into /components if we want to keep it this way
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{hasDownloads ? (
|
{hasDownloads ? (
|
||||||
<FileList fileInfos={fileInfos} sortBy={sortBy} page={PAGES.DOWNLOADED} />
|
<div className="card">
|
||||||
|
<FileList persistedStorageKey="file-list-downloaded" uris={downloadedUris} loading={fetching} />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
|
@ -40,6 +38,5 @@ class FileListDownloaded extends React.PureComponent<Props> {
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default FileListDownloaded;
|
export default FileListDownloaded;
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectIsFetchingClaimListMine, selectFileListPublishedSort, selectMyClaimsWithoutChannels } from 'lbry-redux';
|
import { selectIsFetchingClaimListMine, selectMyClaimsWithoutChannels } from 'lbry-redux';
|
||||||
import { doCheckPendingPublishes } from 'redux/actions/publish';
|
import { doCheckPendingPublishes } from 'redux/actions/publish';
|
||||||
import FileListPublished from './view';
|
import FileListPublished from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
claims: selectMyClaimsWithoutChannels(state),
|
claims: selectMyClaimsWithoutChannels(state),
|
||||||
fetching: selectIsFetchingClaimListMine(state),
|
fetching: selectIsFetchingClaimListMine(state),
|
||||||
sortBy: selectFileListPublishedSort(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
|
|
@ -1,29 +1,32 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileList from 'component/fileList';
|
import FileList from 'component/fileList';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import { PAGES } from 'lbry-redux';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claims: Array<StreamClaim>,
|
claims: Array<StreamClaim>,
|
||||||
checkPendingPublishes: () => void,
|
checkPendingPublishes: () => void,
|
||||||
fetching: boolean,
|
fetching: boolean,
|
||||||
sortBy: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileListPublished extends React.PureComponent<Props> {
|
function FileListPublished(props: Props) {
|
||||||
componentDidMount() {
|
const { checkPendingPublishes, fetching, claims } = props;
|
||||||
const { checkPendingPublishes } = this.props;
|
|
||||||
checkPendingPublishes();
|
useEffect(() => {
|
||||||
}
|
checkPendingPublishes();
|
||||||
|
}, [checkPendingPublishes]);
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fetching, claims, sortBy } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Page notContained loading={fetching}>
|
<Page notContained loading={fetching}>
|
||||||
{claims && claims.length ? (
|
{claims && claims.length ? (
|
||||||
<FileList checkPending fileInfos={claims} sortByHeight sortBy={sortBy} page={PAGES.PUBLISHED} />
|
<div className="card">
|
||||||
|
<FileList
|
||||||
|
persistedStorageKey="file-list-published"
|
||||||
|
// TODO: adjust selector to only return uris
|
||||||
|
uris={claims.map(info => `lbry://${info.name}#${info.claim_id}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
|
@ -42,6 +45,5 @@ class FileListPublished extends React.PureComponent<Props> {
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default FileListPublished;
|
export default FileListPublished;
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doSearch, selectIsSearching } from 'lbry-redux';
|
import { doSearch, selectIsSearching, makeSelectSearchUris, makeSelectQueryWithOptions, doToast } from 'lbry-redux';
|
||||||
|
import analytics from 'analytics';
|
||||||
import SearchPage from './view';
|
import SearchPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
isSearching: selectIsSearching(state),
|
isSearching: selectIsSearching(state),
|
||||||
|
uris: makeSelectSearchUris(makeSelectQueryWithOptions()(state))(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = {
|
const perform = dispatch => ({
|
||||||
doSearch,
|
doSearch: query => doSearch(query),
|
||||||
};
|
onFeedbackPositive: query => {
|
||||||
|
analytics.apiSearchFeedback(query, 1);
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
message: __('Thanks for the feedback! You help make the app better for everyone.'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onFeedbackNegative: query => {
|
||||||
|
analytics.apiSearchFeedback(query, 0);
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
message: __(
|
||||||
|
'Thanks for the feedback. Mark has been notified and is currently walking over to his computer to work on this.'
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useEffect, Fragment } from 'react';
|
import React, { useEffect, Fragment } from 'react';
|
||||||
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
|
import { isURIValid, normalizeURI } from 'lbry-redux';
|
||||||
import FileTile from 'component/fileTile';
|
import FileListItem from 'component/fileListItem';
|
||||||
import ChannelTile from 'component/channelTile';
|
import FileList from 'component/fileList';
|
||||||
import FileListSearch from 'component/fileListSearch';
|
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import SearchOptions from 'component/searchOptions';
|
import SearchOptions from 'component/searchOptions';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
|
||||||
type Props = { doSearch: string => void, location: UrlLocation };
|
type Props = {
|
||||||
|
doSearch: string => void,
|
||||||
|
location: UrlLocation,
|
||||||
|
uris: Array<string>,
|
||||||
|
onFeedbackNegative: string => void,
|
||||||
|
onFeedbackPositive: string => void,
|
||||||
|
};
|
||||||
|
|
||||||
export default function SearchPage(props: Props) {
|
export default function SearchPage(props: Props) {
|
||||||
const {
|
const {
|
||||||
doSearch,
|
doSearch,
|
||||||
|
uris,
|
||||||
|
onFeedbackPositive,
|
||||||
|
onFeedbackNegative,
|
||||||
location: { search },
|
location: { search },
|
||||||
} = props;
|
} = props;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
|
@ -20,10 +29,8 @@ export default function SearchPage(props: Props) {
|
||||||
const isValid = isURIValid(urlQuery);
|
const isValid = isURIValid(urlQuery);
|
||||||
|
|
||||||
let uri;
|
let uri;
|
||||||
let isChannel;
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
uri = normalizeURI(urlQuery);
|
uri = normalizeURI(urlQuery);
|
||||||
({ isChannel } = parseURI(uri));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -42,19 +49,34 @@ export default function SearchPage(props: Props) {
|
||||||
<Button button="alt" navigate={uri} className="media__uri">
|
<Button button="alt" navigate={uri} className="media__uri">
|
||||||
{uri}
|
{uri}
|
||||||
</Button>
|
</Button>
|
||||||
{isChannel ? (
|
<FileListItem uri={uri} large />
|
||||||
<ChannelTile size="large" isSearchResult uri={uri} />
|
|
||||||
) : (
|
|
||||||
<FileTile size="large" isSearchResult displayHiddenMessage uri={uri} />
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="search__results-wrapper">
|
<div className="card">
|
||||||
<SearchOptions />
|
<FileList
|
||||||
<FileListSearch query={urlQuery} />
|
uris={uris}
|
||||||
<div className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div>
|
header={<SearchOptions />}
|
||||||
|
headerAltControls={
|
||||||
|
<Fragment>
|
||||||
|
<span>{__('Find what you were looking for?')}</span>
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
description={__('Yes')}
|
||||||
|
onClick={() => onFeedbackPositive(urlQuery)}
|
||||||
|
icon={ICONS.YES}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
description={__('No')}
|
||||||
|
onClick={() => onFeedbackNegative(urlQuery)}
|
||||||
|
icon={ICONS.NO}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ShowPage extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
innerContent = (
|
innerContent = (
|
||||||
<Page notContained>
|
<Page>
|
||||||
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
|
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
|
||||||
{!isResolvingUri && <span className="empty">{__("There's nothing available at this location.")}</span>}
|
{!isResolvingUri && <span className="empty">{__("There's nothing available at this location.")}</span>}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -1,45 +1,25 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as settings from 'constants/settings';
|
|
||||||
import {
|
import {
|
||||||
selectSubscriptionClaims,
|
selectSubscriptionClaims,
|
||||||
selectSubscriptions,
|
selectSubscriptions,
|
||||||
selectSubscriptionsBeingFetched,
|
selectSubscriptionsBeingFetched,
|
||||||
selectIsFetchingSubscriptions,
|
selectIsFetchingSubscriptions,
|
||||||
selectUnreadSubscriptions,
|
selectSuggestedChannels,
|
||||||
selectViewMode,
|
|
||||||
selectFirstRunCompleted,
|
|
||||||
selectshowSuggestedSubs,
|
|
||||||
} from 'redux/selectors/subscriptions';
|
} from 'redux/selectors/subscriptions';
|
||||||
import {
|
import { doFetchMySubscriptions, doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
|
||||||
doFetchMySubscriptions,
|
|
||||||
doSetViewMode,
|
|
||||||
doFetchRecommendedSubscriptions,
|
|
||||||
doCompleteFirstRun,
|
|
||||||
doShowSuggestedSubs,
|
|
||||||
} from 'redux/actions/subscriptions';
|
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
|
||||||
import SubscriptionsPage from './view';
|
import SubscriptionsPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
loading: selectIsFetchingSubscriptions(state) || Boolean(Object.keys(selectSubscriptionsBeingFetched(state)).length),
|
loading: selectIsFetchingSubscriptions(state) || Boolean(Object.keys(selectSubscriptionsBeingFetched(state)).length),
|
||||||
subscribedChannels: selectSubscriptions(state),
|
subscribedChannels: selectSubscriptions(state),
|
||||||
autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state),
|
subscriptionContent: selectSubscriptionClaims(state),
|
||||||
allSubscriptions: selectSubscriptionClaims(state),
|
suggestedSubscriptions: selectSuggestedChannels(state),
|
||||||
unreadSubscriptions: selectUnreadSubscriptions(state),
|
|
||||||
viewMode: selectViewMode(state),
|
|
||||||
firstRunCompleted: selectFirstRunCompleted(state),
|
|
||||||
showSuggestedSubs: selectshowSuggestedSubs(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
{
|
{
|
||||||
doFetchMySubscriptions,
|
doFetchMySubscriptions,
|
||||||
doSetClientSetting,
|
|
||||||
doSetViewMode,
|
|
||||||
doFetchRecommendedSubscriptions,
|
doFetchRecommendedSubscriptions,
|
||||||
doCompleteFirstRun,
|
|
||||||
doShowSuggestedSubs,
|
|
||||||
}
|
}
|
||||||
)(SubscriptionsPage);
|
)(SubscriptionsPage);
|
||||||
|
|
|
@ -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
|
// @flow
|
||||||
import * as SETTINGS from 'constants/settings';
|
import React, { useEffect, useState } from 'react';
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import FirstRun from './internal/first-run';
|
import FileList from 'component/fileList';
|
||||||
import UserSubscriptions from './internal/user-subscriptions';
|
import Button from 'component/button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<string>, // The channels a user is subscribed to
|
subscribedChannels: Array<string>, // The channels a user is subscribed to
|
||||||
unreadSubscriptions: Array<{
|
subscriptionContent: Array<{ uri: string, ...StreamClaim }>,
|
||||||
channel: string,
|
suggestedSubscriptions: Array<{ uri: string }>,
|
||||||
uris: Array<string>,
|
|
||||||
}>,
|
|
||||||
allSubscriptions: Array<{ uri: string, ...StreamClaim }>,
|
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
autoDownload: boolean,
|
|
||||||
viewMode: ViewMode,
|
|
||||||
doSetViewMode: ViewMode => void,
|
|
||||||
doFetchMySubscriptions: () => void,
|
doFetchMySubscriptions: () => void,
|
||||||
doSetClientSetting: (string, boolean) => void,
|
|
||||||
doFetchRecommendedSubscriptions: () => void,
|
doFetchRecommendedSubscriptions: () => void,
|
||||||
loadingSuggested: boolean,
|
|
||||||
firstRunCompleted: boolean,
|
|
||||||
doCompleteFirstRun: () => void,
|
|
||||||
doShowSuggestedSubs: () => void,
|
|
||||||
showSuggestedSubs: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class SubscriptionsPage extends PureComponent<Props> {
|
export default function SubscriptionsPage(props: Props) {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
(this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
const {
|
||||||
|
subscriptionContent,
|
||||||
|
subscribedChannels,
|
||||||
doFetchMySubscriptions,
|
doFetchMySubscriptions,
|
||||||
doFetchRecommendedSubscriptions,
|
doFetchRecommendedSubscriptions,
|
||||||
allSubscriptions,
|
suggestedSubscriptions,
|
||||||
firstRunCompleted,
|
loading,
|
||||||
doShowSuggestedSubs,
|
} = props;
|
||||||
} = this.props;
|
const hasSubscriptions = !!subscribedChannels.length;
|
||||||
|
const [showSuggested, setShowSuggested] = useState(!hasSubscriptions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
doFetchMySubscriptions();
|
doFetchMySubscriptions();
|
||||||
doFetchRecommendedSubscriptions();
|
doFetchRecommendedSubscriptions();
|
||||||
|
}, [doFetchMySubscriptions, doFetchRecommendedSubscriptions]);
|
||||||
// For channels that already have subscriptions, show the suggested subs right away
|
|
||||||
// This can probably be removed at a future date, it is just to make it so the "view your x subscriptions" button shows up right away
|
|
||||||
// Existing users will still go through the "first run"
|
|
||||||
if (!firstRunCompleted && allSubscriptions.length) {
|
|
||||||
doShowSuggestedSubs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onAutoDownloadChange(event: SyntheticInputEvent<*>) {
|
|
||||||
this.props.doSetClientSetting(SETTINGS.AUTO_DOWNLOAD, event.target.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
subscribedChannels,
|
|
||||||
allSubscriptions,
|
|
||||||
loading,
|
|
||||||
autoDownload,
|
|
||||||
viewMode,
|
|
||||||
doSetViewMode,
|
|
||||||
loadingSuggested,
|
|
||||||
firstRunCompleted,
|
|
||||||
doCompleteFirstRun,
|
|
||||||
doShowSuggestedSubs,
|
|
||||||
showSuggestedSubs,
|
|
||||||
unreadSubscriptions,
|
|
||||||
} = this.props;
|
|
||||||
const numberOfSubscriptions = subscribedChannels && subscribedChannels.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Only pass in the loading prop if there are no subscriptions
|
<Page>
|
||||||
// If there are any, let the page update in the background
|
<div className="card">
|
||||||
// The loading prop removes children and shows a loading spinner
|
<FileList
|
||||||
<Page notContained loading={loading && !subscribedChannels} className="main--no-padding-top">
|
loading={loading}
|
||||||
{firstRunCompleted ? (
|
header={<h1>{showSuggested ? __('Discover New Channels') : __('Latest From Your Subscriptions')}</h1>}
|
||||||
<UserSubscriptions
|
headerAltControls={
|
||||||
viewMode={viewMode}
|
<Button
|
||||||
doSetViewMode={doSetViewMode}
|
button="alt"
|
||||||
hasSubscriptions={numberOfSubscriptions > 0}
|
label={showSuggested ? hasSubscriptions && __('View Your Subscriptions') : __('Find New Channels')}
|
||||||
subscriptions={allSubscriptions}
|
onClick={() => setShowSuggested(!showSuggested)}
|
||||||
autoDownload={autoDownload}
|
|
||||||
onChangeAutoDownload={this.onAutoDownloadChange}
|
|
||||||
unreadSubscriptions={unreadSubscriptions}
|
|
||||||
loadingSuggested={loadingSuggested}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
}
|
||||||
<FirstRun
|
uris={
|
||||||
showSuggested={showSuggestedSubs}
|
showSuggested
|
||||||
doShowSuggestedSubs={doShowSuggestedSubs}
|
? suggestedSubscriptions.map(sub => sub.uri)
|
||||||
loadingSuggested={loadingSuggested}
|
: subscriptionContent.map(sub => sub.permanent_url)
|
||||||
numberOfSubscriptions={numberOfSubscriptions}
|
}
|
||||||
onFinish={doCompleteFirstRun}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
14
src/ui/page/tags/index.js
Normal file
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 { combineReducers } from 'redux';
|
||||||
import { connectRouter } from 'connected-react-router';
|
import { connectRouter } from 'connected-react-router';
|
||||||
import { claimsReducer, fileInfoReducer, searchReducer, walletReducer, notificationsReducer } from 'lbry-redux';
|
import {
|
||||||
|
claimsReducer,
|
||||||
|
fileInfoReducer,
|
||||||
|
searchReducer,
|
||||||
|
walletReducer,
|
||||||
|
notificationsReducer,
|
||||||
|
tagsReducer,
|
||||||
|
} from 'lbry-redux';
|
||||||
import { userReducer, rewardsReducer, costInfoReducer, blacklistReducer, homepageReducer, statsReducer } from 'lbryinc';
|
import { userReducer, rewardsReducer, costInfoReducer, blacklistReducer, homepageReducer, statsReducer } from 'lbryinc';
|
||||||
import appReducer from 'redux/reducers/app';
|
import appReducer from 'redux/reducers/app';
|
||||||
import availabilityReducer from 'redux/reducers/availability';
|
import availabilityReducer from 'redux/reducers/availability';
|
||||||
|
@ -27,6 +34,7 @@ export default history =>
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
stats: statsReducer,
|
stats: statsReducer,
|
||||||
subscriptions: subscriptionsReducer,
|
subscriptions: subscriptionsReducer,
|
||||||
|
tags: tagsReducer,
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
wallet: walletReducer,
|
wallet: walletReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -391,12 +391,6 @@ export function doConditionalAuthNavigate(newSession) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doToggleEnhancedLayout() {
|
|
||||||
return {
|
|
||||||
type: ACTIONS.ENNNHHHAAANNNCEEE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doToggleSearchExpanded() {
|
export function doToggleSearchExpanded() {
|
||||||
return {
|
return {
|
||||||
type: ACTIONS.TOGGLE_SEARCH_EXPANDED,
|
type: ACTIONS.TOGGLE_SEARCH_EXPANDED,
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
selectBalance,
|
selectBalance,
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
parseURI,
|
parseURI,
|
||||||
creditsToString,
|
|
||||||
doError,
|
doError,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
|
@ -293,13 +292,7 @@ export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize:
|
||||||
data: { uri, page },
|
data: { uri, page },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { claimName, claimId } = parseURI(uri);
|
Lbry.claim_search({ channel: uri, is_controlling: true, page, page_size: pageSize }).then(result => {
|
||||||
let channelName = claimName;
|
|
||||||
if (claimId) {
|
|
||||||
channelName += `#${claimId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
Lbry.claim_search({ channel_name: channelName, page, page_size: pageSize }).then(result => {
|
|
||||||
const { items: claimsInChannel, page: returnedPage } = result;
|
const { items: claimsInChannel, page: returnedPage } = result;
|
||||||
|
|
||||||
if (claimsInChannel && claimsInChannel.length) {
|
if (claimsInChannel && claimsInChannel.length) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
|
||||||
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
|
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
|
||||||
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content';
|
import { doPurchaseUri } from 'redux/actions/content';
|
||||||
|
|
||||||
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
|
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
|
||||||
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
|
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
|
||||||
|
@ -35,8 +35,7 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
|
||||||
Lbryio.call('subscription', 'list')
|
Lbryio.call('subscription', 'list')
|
||||||
.then(dbSubscriptions => {
|
.then(dbSubscriptions => {
|
||||||
const storedSubscriptions = dbSubscriptions || [];
|
const storedSubscriptions = dbSubscriptions || [];
|
||||||
|
// // User has no subscriptions in db or redux
|
||||||
// User has no subscriptions in db or redux
|
|
||||||
if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
|
if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -45,25 +44,12 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
|
||||||
// If something is in the db, but not in redux, add it to redux
|
// If something is in the db, but not in redux, add it to redux
|
||||||
// If something is in redux, but not in the db, add it to the db
|
// If something is in redux, but not in the db, add it to the db
|
||||||
if (storedSubscriptions.length !== reduxSubscriptions.length) {
|
if (storedSubscriptions.length !== reduxSubscriptions.length) {
|
||||||
const dbSubMap = {};
|
|
||||||
const reduxSubMap = {};
|
const reduxSubMap = {};
|
||||||
const subsNotInDB = [];
|
|
||||||
const subscriptionsToReturn = reduxSubscriptions.slice();
|
const subscriptionsToReturn = reduxSubscriptions.slice();
|
||||||
|
|
||||||
storedSubscriptions.forEach(sub => {
|
|
||||||
dbSubMap[sub.claim_id] = 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
reduxSubscriptions.forEach(sub => {
|
reduxSubscriptions.forEach(sub => {
|
||||||
const { claimId } = parseURI(sub.uri);
|
const { claimId } = parseURI(sub.uri);
|
||||||
reduxSubMap[claimId] = 1;
|
reduxSubMap[claimId] = 1;
|
||||||
|
|
||||||
if (!dbSubMap[claimId]) {
|
|
||||||
subsNotInDB.push({
|
|
||||||
claim_id: claimId,
|
|
||||||
channel_name: sub.channelName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
storedSubscriptions.forEach(sub => {
|
storedSubscriptions.forEach(sub => {
|
||||||
|
@ -73,13 +59,7 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload)))
|
return subscriptionsToReturn;
|
||||||
.then(() => subscriptionsToReturn)
|
|
||||||
.catch(
|
|
||||||
() =>
|
|
||||||
// let it fail, we will try again when the navigate to the subscriptions page
|
|
||||||
subscriptionsToReturn
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB is already synced, just return the subscriptions in redux
|
// DB is already synced, just return the subscriptions in redux
|
||||||
|
@ -223,10 +203,9 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
||||||
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
|
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { claimId } = parseURI(subscriptionUri);
|
|
||||||
|
|
||||||
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
|
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
|
||||||
Lbry.claim_search({ channel_id: claimId, page: 1, page_size: PAGE_SIZE }).then(claimListByChannel => {
|
Lbry.claim_search({ channel: subscriptionUri, is_controlling: true, page: 1, page_size: PAGE_SIZE }).then(
|
||||||
|
claimListByChannel => {
|
||||||
const { items: claimsInChannel } = claimListByChannel;
|
const { items: claimsInChannel } = claimListByChannel;
|
||||||
|
|
||||||
// may happen if subscribed to an abandoned channel or an empty channel
|
// may happen if subscribed to an abandoned channel or an empty channel
|
||||||
|
@ -301,7 +280,8 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
||||||
page: 1,
|
page: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
|
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
@ -394,13 +374,3 @@ export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doCompleteFirstRun = () => (dispatch: Dispatch) =>
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const doShowSuggestedSubs = () => (dispatch: Dispatch) =>
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,14 +2,7 @@
|
||||||
|
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
// @if TARGET='app'
|
|
||||||
// $FlowFixMe
|
|
||||||
import { remote } from 'electron';
|
import { remote } from 'electron';
|
||||||
// @endif
|
|
||||||
// @if TARGET='web'
|
|
||||||
// $FlowFixMe
|
|
||||||
import { remote } from 'web/stubs';
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
const win = remote.BrowserWindow.getFocusedWindow();
|
const win = remote.BrowserWindow.getFocusedWindow();
|
||||||
|
@ -43,7 +36,6 @@ export type AppState = {
|
||||||
isUpgradeAvailable: ?boolean,
|
isUpgradeAvailable: ?boolean,
|
||||||
isUpgradeSkipped: ?boolean,
|
isUpgradeSkipped: ?boolean,
|
||||||
hasClickedComment: boolean,
|
hasClickedComment: boolean,
|
||||||
enhancedLayout: boolean,
|
|
||||||
searchOptionsExpanded: boolean,
|
searchOptionsExpanded: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -228,11 +220,6 @@ reducers[ACTIONS.AUTHENTICATION_FAILURE] = state =>
|
||||||
modal: MODALS.AUTHENTICATION_FAILURE,
|
modal: MODALS.AUTHENTICATION_FAILURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.ENNNHHHAAANNNCEEE] = state =>
|
|
||||||
Object.assign({}, state, {
|
|
||||||
enhancedLayout: !state.enhancedLayout,
|
|
||||||
});
|
|
||||||
|
|
||||||
reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state =>
|
reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
searchOptionsExpanded: !state.searchOptionsExpanded,
|
searchOptionsExpanded: !state.searchOptionsExpanded,
|
||||||
|
|
|
@ -19,11 +19,9 @@ const defaultState = {
|
||||||
[SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
|
[SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
|
||||||
[SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false),
|
[SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false),
|
||||||
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, false),
|
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, false),
|
||||||
[SETTINGS.INVITE_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.INVITE_ACKNOWLEDGED, false),
|
|
||||||
[SETTINGS.FIRST_RUN_COMPLETED]: getLocalStorageSetting(SETTINGS.FIRST_RUN_COMPLETED, false),
|
|
||||||
[SETTINGS.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run
|
[SETTINGS.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run
|
||||||
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
|
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
|
||||||
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'dark'),
|
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'light'),
|
||||||
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
|
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
|
||||||
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
|
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
|
||||||
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
|
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
|
||||||
|
|
|
@ -134,14 +134,6 @@ export default handleActions(
|
||||||
...state,
|
...state,
|
||||||
loadingSuggested: false,
|
loadingSuggested: false,
|
||||||
}),
|
}),
|
||||||
[ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED]: (state: SubscriptionState): SubscriptionState => ({
|
|
||||||
...state,
|
|
||||||
firstRunCompleted: true,
|
|
||||||
}),
|
|
||||||
[ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
|
|
||||||
...state,
|
|
||||||
showSuggestedSubs: true,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
defaultState
|
defaultState
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
|
||||||
|
|
||||||
export const selectState = state => state.app || {};
|
export const selectState = state => state.app || {};
|
||||||
|
|
||||||
|
@ -121,20 +119,7 @@ export const selectModal = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectEnhancedLayout = createSelector(
|
|
||||||
selectState,
|
|
||||||
state => state.enhancedLayout
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectSearchOptionsExpanded = createSelector(
|
export const selectSearchOptionsExpanded = createSelector(
|
||||||
selectState,
|
selectState,
|
||||||
state => state.searchOptionsExpanded
|
state => state.searchOptionsExpanded
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectShouldShowInviteGuide = createSelector(
|
|
||||||
makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED),
|
|
||||||
makeSelectClientSetting(SETTINGS.INVITE_ACKNOWLEDGED),
|
|
||||||
(firstRunCompleted, inviteAcknowledged) => {
|
|
||||||
return firstRunCompleted ? !inviteAcknowledged : false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
parseURI,
|
parseURI,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { swapKeyAndValue } from 'util/swap-json';
|
import { swapKeyAndValue } from 'util/swap-json';
|
||||||
import { shuffleArray } from 'util/shuffleArray';
|
|
||||||
|
|
||||||
// Returns the entire subscriptions state
|
// Returns the entire subscriptions state
|
||||||
const selectState = state => state.subscriptions || {};
|
const selectState = state => state.subscriptions || {};
|
||||||
|
@ -86,13 +85,10 @@ export const selectSuggestedChannels = createSelector(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.keys(suggestedChannels)
|
return Object.keys(suggestedChannels).map(uri => ({
|
||||||
.map(uri => ({
|
|
||||||
uri,
|
uri,
|
||||||
label: suggestedChannels[uri],
|
label: suggestedChannels[uri],
|
||||||
}))
|
}));
|
||||||
.sort(shuffleArray)
|
|
||||||
.slice(0, 5);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
@import 'init/gui';
|
@import 'init/gui';
|
||||||
@import 'component/animation';
|
@import 'component/animation';
|
||||||
@import 'component/badge';
|
@import 'component/badge';
|
||||||
@import 'component/banner';
|
|
||||||
@import 'component/button';
|
@import 'component/button';
|
||||||
@import 'component/card';
|
@import 'component/card';
|
||||||
@import 'component/channel';
|
@import 'component/channel';
|
||||||
|
@ -19,6 +18,8 @@
|
||||||
@import 'component/dat-gui';
|
@import 'component/dat-gui';
|
||||||
@import 'component/expandable';
|
@import 'component/expandable';
|
||||||
@import 'component/file-download';
|
@import 'component/file-download';
|
||||||
|
@import 'component/file-list';
|
||||||
|
@import 'component/file-properties';
|
||||||
@import 'component/file-render';
|
@import 'component/file-render';
|
||||||
@import 'component/form-field';
|
@import 'component/form-field';
|
||||||
@import 'component/header';
|
@import 'component/header';
|
||||||
|
@ -33,7 +34,6 @@
|
||||||
@import 'component/notice';
|
@import 'component/notice';
|
||||||
@import 'component/pagination';
|
@import 'component/pagination';
|
||||||
@import 'component/placeholder';
|
@import 'component/placeholder';
|
||||||
@import 'component/scrollbar';
|
|
||||||
@import 'component/search';
|
@import 'component/search';
|
||||||
@import 'component/snack-bar';
|
@import 'component/snack-bar';
|
||||||
@import 'component/spinner';
|
@import 'component/spinner';
|
||||||
|
@ -42,6 +42,7 @@
|
||||||
@import 'component/syntax-highlighter';
|
@import 'component/syntax-highlighter';
|
||||||
@import 'component/table';
|
@import 'component/table';
|
||||||
@import 'component/tabs';
|
@import 'component/tabs';
|
||||||
|
@import 'component/tags';
|
||||||
@import 'component/time';
|
@import 'component/time';
|
||||||
@import 'component/toggle';
|
@import 'component/toggle';
|
||||||
@import 'component/tooltip';
|
@import 'component/tooltip';
|
||||||
|
|
|
@ -1 +1,18 @@
|
||||||
@import '~@lbry/components/sass/badge/_index.scss';
|
@import '~@lbry/components/sass/badge/_index.scss';
|
||||||
|
|
||||||
|
.badge--tag {
|
||||||
|
@extend .badge;
|
||||||
|
background-color: lighten($lbry-teal-5, 55%);
|
||||||
|
color: darken($lbry-teal-5, 20%);
|
||||||
|
|
||||||
|
[data-mode='dark'] & {
|
||||||
|
color: lighten($lbry-teal-5, 60%);
|
||||||
|
background-color: rgba($lbry-teal-5, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--alert {
|
||||||
|
@extend .badge;
|
||||||
|
background-color: $lbry-red-2;
|
||||||
|
color: $lbry-white;
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.button__content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
stroke-width: 1.9;
|
stroke-width: 1.9;
|
||||||
width: 1.2rem;
|
|
||||||
height: 1.2rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
color: $lbry-gray-5;
|
color: $lbry-gray-5;
|
||||||
|
|
||||||
|
@ -23,12 +15,6 @@
|
||||||
height: 1.4rem;
|
height: 1.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle icons on the left or right side of the button label
|
|
||||||
svg + .button__label,
|
|
||||||
.button__label + svg {
|
|
||||||
margin-left: var(--spacing-vertical-miniscule);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--primary {
|
.button--primary {
|
||||||
|
@ -56,16 +42,57 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button--alt {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.button--uri-indicator {
|
.button--uri-indicator {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
overflow: hidden;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $lbry-teal-3;
|
color: $lbry-teal-5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button--close {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--spacing-miniscule);
|
||||||
|
right: var(--spacing-miniscule);
|
||||||
|
padding: 0.3rem;
|
||||||
|
transition: all var(--transition-duration) var(--transition-style);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $lbry-red-3;
|
||||||
|
color: $lbry-white;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--subscribe {
|
||||||
|
vertical-align: text-top;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button__label {
|
||||||
|
// white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle icons on the left or right side of the button label
|
||||||
|
svg + .button__label,
|
||||||
|
.button__label + svg {
|
||||||
|
margin-left: var(--spacing-miniscule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
.card {
|
.card {
|
||||||
background-color: $lbry-white;
|
background-color: $lbry-white;
|
||||||
margin-bottom: var(--spacing-vertical-xlarge);
|
margin-bottom: var(--spacing-xlarge);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--card-radius);
|
border-radius: var(--card-radius);
|
||||||
box-shadow: var(--card-box-shadow) $lbry-gray-1;
|
box-shadow: var(--card-box-shadow) $lbry-gray-1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
html[data-mode='dark'] & {
|
html[data-mode='dark'] & {
|
||||||
background-color: rgba($lbry-white, 0.1);
|
background-color: lighten($lbry-black, 5%);
|
||||||
box-shadow: var(--card-box-shadow) darken($lbry-gray-1, 80%);
|
box-shadow: var(--card-box-shadow) darken($lbry-gray-1, 80%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,10 +22,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--section {
|
.card--section {
|
||||||
padding: var(--spacing-vertical-large);
|
position: relative;
|
||||||
|
padding: var(--spacing-large);
|
||||||
|
|
||||||
.card__content:not(:last-of-type) {
|
.card__content:not(:last-of-type) {
|
||||||
margin-bottom: var(--spacing-vertical-large);
|
margin-bottom: var(--spacing-large);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +41,10 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card--modal {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
// C A R D
|
// C A R D
|
||||||
// A C T I O N S
|
// A C T I O N S
|
||||||
|
|
||||||
|
@ -47,7 +53,7 @@
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
|
|
||||||
> *:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-right: var(--spacing-vertical-medium);
|
margin-right: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +80,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__actions--top-space {
|
.card__actions--top-space {
|
||||||
padding-top: var(--spacing-vertical-small);
|
padding-top: var(--spacing-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
// C A R D
|
// C A R D
|
||||||
|
@ -84,13 +90,12 @@
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
|
||||||
p:not(:last-child) {
|
p:not(:last-child) {
|
||||||
margin-bottom: var(--spacing-vertical-medium);
|
margin-bottom: var(--spacing-medium);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.card__content--large {
|
||||||
bottom: -0.15rem;
|
font-size: 4rem;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// C A R D
|
// C A R D
|
||||||
|
@ -100,25 +105,17 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:not(.card__header--flat) {
|
&:not(.card__header--flat) {
|
||||||
margin-bottom: var(--spacing-vertical-medium);
|
margin-bottom: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// C A R D
|
|
||||||
// I N T E R N A L
|
|
||||||
|
|
||||||
.card__internal-links {
|
|
||||||
top: 2rem;
|
|
||||||
right: 2rem;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
// C A R D
|
// C A R D
|
||||||
// L I S T
|
// L I S T
|
||||||
|
|
||||||
.card__list {
|
.card__list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-vertical-medium);
|
grid-gap: var(--spacing-medium);
|
||||||
|
margin-top: var(--spacing-large);
|
||||||
|
|
||||||
// Depending on screen width, the amount of items in
|
// Depending on screen width, the amount of items in
|
||||||
// each row change and are auto-sized
|
// each row change and are auto-sized
|
||||||
|
@ -135,31 +132,21 @@
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 7), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 7), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1051px) and (max-width: 1550px) {
|
@media (min-width: 1200px) and (max-width: 1550px) {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 6), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 6), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 901px) and (max-width: 1050px) {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 751px) and (max-width: 900px) {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 4), 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 750px) {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 3), 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__list--rewards {
|
.card__list--rewards {
|
||||||
column-count: 2;
|
column-count: 2;
|
||||||
column-gap: var(--spacing-vertical-medium);
|
column-gap: var(--spacing-medium);
|
||||||
margin-bottom: var(--spacing-vertical-large);
|
margin-bottom: var(--spacing-large);
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 0 var(--spacing-vertical-medium);
|
margin: 0 0 var(--spacing-medium);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,8 +156,7 @@
|
||||||
|
|
||||||
.card__message {
|
.card__message {
|
||||||
border-left: 0.5rem solid;
|
border-left: 0.5rem solid;
|
||||||
padding: var(--spacing-vertical-medium) var(--spacing-vertical-medium) var(--spacing-vertical-medium)
|
padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-medium) var(--spacing-large);
|
||||||
var(--spacing-vertical-large);
|
|
||||||
|
|
||||||
&:not(&--error):not(&--failure):not(&--success) {
|
&:not(&--error):not(&--failure):not(&--success) {
|
||||||
background-color: rgba($lbry-teal-1, 0.1);
|
background-color: rgba($lbry-teal-1, 0.1);
|
||||||
|
@ -198,22 +184,24 @@
|
||||||
|
|
||||||
.card__subtitle {
|
.card__subtitle {
|
||||||
@extend .help;
|
@extend .help;
|
||||||
background-color: lighten($lbry-gray-1, 7%);
|
color: darken($lbry-gray-5, 25%);
|
||||||
color: darken($lbry-gray-5, 30%);
|
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
margin-bottom: var(--spacing-vertical-small);
|
margin-bottom: var(--spacing-small);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: var(--spacing-vertical-small);
|
margin-bottom: var(--spacing-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
bottom: -0.12rem;
|
bottom: -0.12rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mode='dark'] & {
|
[data-mode='dark'] & {
|
||||||
background-color: darken($lbry-gray-5, 20%);
|
// TODO: dark
|
||||||
|
// background-color: darken($lbry-gray-5, 20%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,10 +211,10 @@
|
||||||
.card__title {
|
.card__title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: var(--spacing-vertical-medium);
|
margin-bottom: var(--spacing-medium);
|
||||||
|
|
||||||
+ .card__content {
|
+ .card__content {
|
||||||
margin-top: var(--spacing-vertical-medium);
|
margin-top: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +223,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& > *:not(:last-child) {
|
& > *:not(:last-child) {
|
||||||
margin-right: var(--spacing-vertical-medium);
|
margin-right: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ $metadata-z-index: 1;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
color: $lbry-white;
|
color: $lbry-white;
|
||||||
|
border-top-left-radius: var(--card-radius);
|
||||||
|
border-top-right-radius: var(--card-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-cover__custom {
|
.channel-cover__custom {
|
||||||
|
@ -24,13 +26,19 @@ $metadata-z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-thumbnail {
|
.channel-thumbnail {
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
left: var(--spacing-main-padding);
|
height: 5.3rem;
|
||||||
height: var(--channel-thumbnail-size);
|
width: 5.4rem;
|
||||||
width: var(--channel-thumbnail-size);
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
margin-right: var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel__thumbnail--channel-page {
|
||||||
|
position: absolute;
|
||||||
|
height: var(--channel-thumbnail-width);
|
||||||
|
width: var(--channel-thumbnail-width);
|
||||||
box-shadow: 0px 8px 40px -3px $lbry-black;
|
box-shadow: 0px 8px 40px -3px $lbry-black;
|
||||||
|
left: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-thumbnail__custom {
|
.channel-thumbnail__custom {
|
||||||
|
@ -44,7 +52,7 @@ $metadata-z-index: 1;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
margin-bottom: -1px;
|
// margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-thumbnail,
|
.channel-thumbnail,
|
||||||
|
@ -70,14 +78,15 @@ $metadata-z-index: 1;
|
||||||
z-index: $metadata-z-index;
|
z-index: $metadata-z-index;
|
||||||
// Jump over the thumbnail photo because it is absolutely positioned
|
// Jump over the thumbnail photo because it is absolutely positioned
|
||||||
// Then add normal page spacing, _then_ add the actual padding
|
// Then add normal page spacing, _then_ add the actual padding
|
||||||
margin-left: calc(var(--channel-thumbnail-size) + var(--spacing-main-padding));
|
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-large));
|
||||||
padding-left: var(--spacing-vertical-large);
|
// padding-left: var(--spacing-large);
|
||||||
padding-bottom: var(--spacing-vertical-medium);
|
padding-bottom: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel__title {
|
.channel__title {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
margin-right: var(--spacing-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel__url {
|
.channel__url {
|
||||||
|
@ -85,3 +94,8 @@ $metadata-z-index: 1;
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
color: rgba($lbry-white, 0.75);
|
color: rgba($lbry-white, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: rename
|
||||||
|
.channel__data {
|
||||||
|
min-height: 10rem;
|
||||||
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 var(--spacing-vertical-large);
|
padding: 0 var(--spacing-large);
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue