Extend markdown support for LBRY urls #2521
193 changed files with 3530 additions and 3732 deletions
|
@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [0.33.0] - [Unreleased]
|
||||
## [0.33.1] - [2019-06-12]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix reflector bugs on failed attempts and improve wallet connection with [LBRY SDK patch](https://github.com/lbryio/lbry/releases/tag/v0.37.4)
|
||||
|
||||
## [0.33.0] - [2019-06-04]
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "LBRY",
|
||||
"version": "0.33.0",
|
||||
"version": "0.33.1",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
|
@ -40,6 +40,8 @@
|
|||
"postinstall": "electron-builder install-app-deps && node ./build/downloadDaemon.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reach/menu-button": "^0.1.18",
|
||||
"@reach/tooltip": "^0.2.1",
|
||||
"electron-dl": "^1.11.0",
|
||||
"electron-log": "^2.2.12",
|
||||
"electron-updater": "^4.0.6",
|
||||
|
@ -60,7 +62,7 @@
|
|||
"@exponent/electron-cookies": "^2.0.0",
|
||||
"@hot-loader/react-dom": "16.8",
|
||||
"@lbry/color": "^1.0.2",
|
||||
"@lbry/components": "^2.7.0",
|
||||
"@lbry/components": "^2.7.2",
|
||||
"@reach/rect": "^0.2.1",
|
||||
"@reach/tabs": "^0.1.5",
|
||||
"@reach/tooltip": "^0.2.1",
|
||||
|
@ -120,7 +122,7 @@
|
|||
"jsmediatags": "^3.8.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#02f6918238110726c0b3b4248c61a84ac0b969e3",
|
||||
"lbry-redux": "lbryio/lbry-redux#6a447d0542d23b9a37e266f5f85d3bde5297a451",
|
||||
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
@ -154,6 +156,7 @@
|
|||
"react-router": "^5.0.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-simplemde-editor": "^4.0.0",
|
||||
"react-spring": "^8.0.20",
|
||||
"react-toggle": "^4.0.2",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^4.8.0",
|
||||
|
@ -195,7 +198,7 @@
|
|||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.37.2",
|
||||
"lbrynetDaemonVersion": "0.38.0rc7",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet"
|
||||
|
|
|
@ -84,19 +84,6 @@ export default appState => {
|
|||
window.loadURL(rendererURL + deepLinkingURI);
|
||||
setupBarMenu();
|
||||
|
||||
// Windows back/forward mouse navigation
|
||||
window.on('app-command', (e, cmd) => {
|
||||
switch (cmd) {
|
||||
case 'browser-backward':
|
||||
window.webContents.send('navigate-backward', null);
|
||||
break;
|
||||
case 'browser-forward':
|
||||
window.webContents.send('navigate-forward', null);
|
||||
break;
|
||||
default: // Do nothing
|
||||
}
|
||||
});
|
||||
|
||||
window.on('close', event => {
|
||||
if (!appState.isQuitting && !appState.autoUpdateAccepted) {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
export const clipboard = () => {
|
||||
throw 'Fix me!';
|
||||
throw new Error('Fix me!');
|
||||
};
|
||||
|
||||
export const ipcRenderer = () => {
|
||||
throw 'Fix me!';
|
||||
};
|
||||
|
||||
export const remote = () => {
|
||||
throw 'Fix me!';
|
||||
throw new Error('Fix me!');
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const callable = () => {
|
||||
throw Error('Need to fix this stub');
|
||||
};
|
||||
const returningCallable = value => () => value;
|
||||
|
||||
export const remote = {
|
||||
dialog: {
|
||||
|
|
|
@ -6,6 +6,8 @@ import { history } from './store';
|
|||
import ElectronCookies from '@exponent/electron-cookies';
|
||||
// @endif
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
type Analytics = {
|
||||
pageView: string => void,
|
||||
setUser: Object => void,
|
||||
|
@ -61,18 +63,20 @@ const analytics: Analytics = {
|
|||
}
|
||||
},
|
||||
apiLogSearch: () => {
|
||||
if (analyticsEnabled) {
|
||||
if (analyticsEnabled && isProduction) {
|
||||
Lbryio.call('event', 'search');
|
||||
}
|
||||
},
|
||||
apiLogPublish: () => {
|
||||
if (analyticsEnabled) {
|
||||
if (analyticsEnabled && isProduction) {
|
||||
Lbryio.call('event', 'publish');
|
||||
}
|
||||
},
|
||||
apiSearchFeedback: (query, vote) => {
|
||||
// We don't need to worry about analytics enabled here because users manually click on the button to provide feedback
|
||||
Lbryio.call('feedback', 'search', { query, vote });
|
||||
if (isProduction) {
|
||||
// We don't need to worry about analytics enabled here because users manually click on the button to provide feedback
|
||||
Lbryio.call('feedback', 'search', { query, vote });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
import { hot } from 'react-hot-loader/root';
|
||||
import { connect } from 'react-redux';
|
||||
import { doUpdateBlockHeight, doError } from 'lbry-redux';
|
||||
import { doToggleEnhancedLayout } from 'redux/actions/app';
|
||||
import { selectUser } from 'lbryinc';
|
||||
import { selectUser, doRewardList, doFetchRewardedContent } from 'lbryinc';
|
||||
import { selectThemePath } from 'redux/selectors/settings';
|
||||
import { selectEnhancedLayout } from 'redux/selectors/app';
|
||||
import App from './view';
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
theme: selectThemePath(state),
|
||||
enhancedLayout: selectEnhancedLayout(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
alertError: errorList => dispatch(doError(errorList)),
|
||||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||
toggleEnhancedLayout: () => dispatch(doToggleEnhancedLayout()),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
});
|
||||
|
||||
export default hot(
|
||||
|
|
|
@ -1,85 +1,54 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Router from 'component/router/index';
|
||||
import ModalRouter from 'modal/modalRouter';
|
||||
import ReactModal from 'react-modal';
|
||||
import SideBar from 'component/sideBar';
|
||||
import Header from 'component/header';
|
||||
import { openContextMenu } from 'util/context-menu';
|
||||
import EnhancedLayoutListener from 'util/enhanced-layout';
|
||||
import useKonamiListener from 'util/enhanced-layout';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
||||
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;
|
||||
|
||||
type Props = {
|
||||
alertError: (string | {}) => void,
|
||||
pageTitle: ?string,
|
||||
language: string,
|
||||
theme: string,
|
||||
updateBlockHeight: () => void,
|
||||
toggleEnhancedLayout: () => void,
|
||||
enhancedLayout: boolean,
|
||||
fetchRewards: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
};
|
||||
|
||||
class App extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
const { alertError, theme } = this.props;
|
||||
function App(props: Props) {
|
||||
const { theme, fetchRewards, fetchRewardedContent } = props;
|
||||
const appRef = useRef();
|
||||
const isEnhancedLayout = useKonamiListener();
|
||||
|
||||
// TODO: create type for this object
|
||||
// it lives in jsonrpc.js
|
||||
document.addEventListener('unhandledError', (event: any) => {
|
||||
alertError(event.detail);
|
||||
});
|
||||
useEffect(() => {
|
||||
ReactModal.setAppElement(appRef.current);
|
||||
fetchRewards();
|
||||
fetchRewardedContent();
|
||||
}, [fetchRewards, fetchRewardedContent]);
|
||||
|
||||
useEffect(() => {
|
||||
// $FlowFixMe
|
||||
document.documentElement.setAttribute('data-mode', theme);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
componentDidMount() {
|
||||
const { updateBlockHeight, toggleEnhancedLayout } = this.props;
|
||||
return (
|
||||
<div ref={appRef} onContextMenu={e => openContextMenu(e)}>
|
||||
<Header />
|
||||
|
||||
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 (
|
||||
<div id="window" onContextMenu={e => openContextMenu(e)}>
|
||||
<Header />
|
||||
<SideBar />
|
||||
|
||||
<div className="main-wrapper">
|
||||
<div className="main-wrapper">
|
||||
<div className="main-wrapper-inner">
|
||||
<Router />
|
||||
<SideBar />
|
||||
</div>
|
||||
|
||||
<ModalRouter />
|
||||
{enhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<ModalRouter />
|
||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -71,8 +71,8 @@ class Button extends React.PureComponent<Props> {
|
|||
'button--primary': button === 'primary',
|
||||
'button--secondary': button === 'secondary',
|
||||
'button--alt': button === 'alt',
|
||||
'button--danger': button === 'danger',
|
||||
'button--inverse': button === 'inverse',
|
||||
'button--close': button === 'close',
|
||||
'button--disabled': disabled,
|
||||
'button--link': button === 'link',
|
||||
'button--constrict': constrict,
|
||||
|
@ -115,6 +115,7 @@ class Button extends React.PureComponent<Props> {
|
|||
exact
|
||||
to={path}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchClaimsByChannel } from 'redux/actions/content';
|
||||
import { makeSelectCategoryListUris } from 'redux/selectors/content';
|
||||
import { makeSelectFetchingChannelClaims, doResolveUris } from 'lbry-redux';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import CategoryList from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
urisInList: makeSelectCategoryListUris(props.uris, props.categoryLink)(state),
|
||||
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
|
||||
resolveUris: uris => dispatch(doResolveUris(uris, true)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(CategoryList);
|
|
@ -1,316 +0,0 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import ToolTip from 'component/common/tooltip';
|
||||
import FileCard from 'component/fileCard';
|
||||
import Button from 'component/button';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import throttle from 'util/throttle';
|
||||
import { formatLbryUriForWeb } from 'util/uri';
|
||||
|
||||
type Props = {
|
||||
category: string,
|
||||
categoryLink: ?string,
|
||||
fetching: boolean,
|
||||
obscureNsfw: boolean,
|
||||
currentPageAttributes: { scrollY: number },
|
||||
fetchChannel: string => void,
|
||||
urisInList: ?Array<string>,
|
||||
resolveUris: (Array<string>) => void,
|
||||
lazyLoad: boolean, // only fetch rows if they are on the screen
|
||||
};
|
||||
|
||||
type State = {
|
||||
canScrollNext: boolean,
|
||||
canScrollPrevious: boolean,
|
||||
};
|
||||
|
||||
class CategoryList extends PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
categoryLink: undefined,
|
||||
lazyLoad: false,
|
||||
};
|
||||
|
||||
scrollWrapper: { current: null | HTMLUListElement };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
canScrollPrevious: false,
|
||||
canScrollNext: true,
|
||||
};
|
||||
|
||||
(this: any).handleScrollNext = this.handleScrollNext.bind(this);
|
||||
(this: any).handleScrollPrevious = this.handleScrollPrevious.bind(this);
|
||||
(this: any).handleArrowButtonsOnScroll = this.handleArrowButtonsOnScroll.bind(this);
|
||||
// (this: any).handleResolveOnScroll = this.handleResolveOnScroll.bind(this);
|
||||
|
||||
this.scrollWrapper = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { fetching, categoryLink, fetchChannel, resolveUris, urisInList, lazyLoad } = this.props;
|
||||
if (!fetching && categoryLink && (!urisInList || !urisInList.length)) {
|
||||
// Only fetch the channels claims if no urisInList are specifically passed in
|
||||
// This allows setting a channel link and and passing in a custom list of urisInList (featured content usually works this way)
|
||||
fetchChannel(categoryLink);
|
||||
}
|
||||
|
||||
const scrollWrapper = this.scrollWrapper.current;
|
||||
if (scrollWrapper) {
|
||||
scrollWrapper.addEventListener('scroll', throttle(this.handleArrowButtonsOnScroll, 500));
|
||||
|
||||
if (!urisInList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lazyLoad) {
|
||||
if (window.innerHeight > scrollWrapper.offsetTop) {
|
||||
resolveUris(urisInList);
|
||||
}
|
||||
} else {
|
||||
resolveUris(urisInList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The old lazy loading for home page relied on the navigation reducers copy of the scroll height
|
||||
// Keeping it commented out for now to try and find a better way for better TTI on the homepage
|
||||
// componentDidUpdate(prevProps: Props) {
|
||||
// const {scrollY: previousScrollY} = prevProps.currentPageAttributes;
|
||||
// const {scrollY} = this.props.currentPageAttributes;
|
||||
|
||||
// if(scrollY > previousScrollY) {
|
||||
// this.handleResolveOnScroll();
|
||||
// }
|
||||
// }
|
||||
|
||||
// handleResolveOnScroll() {
|
||||
// const {
|
||||
// urisInList,
|
||||
// resolveUris,
|
||||
// currentPageAttributes: {scrollY},
|
||||
// } = this.props;
|
||||
|
||||
// const scrollWrapper = this.scrollWrapper.current;
|
||||
// if(!scrollWrapper) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const shouldResolve = window.innerHeight > scrollWrapper.offsetTop - scrollY;
|
||||
// if(shouldResolve && urisInList) {
|
||||
// resolveUris(urisInList);
|
||||
// }
|
||||
// }
|
||||
|
||||
handleArrowButtonsOnScroll() {
|
||||
// Determine if the arrow buttons should be disabled
|
||||
const scrollWrapper = this.scrollWrapper.current;
|
||||
if (scrollWrapper) {
|
||||
// firstElementChild and lastElementChild will always exist
|
||||
// $FlowFixMe
|
||||
const hasHiddenCardToLeft = !this.isCardVisible(scrollWrapper.firstElementChild);
|
||||
// $FlowFixMe
|
||||
const hasHiddenCardToRight = !this.isCardVisible(scrollWrapper.lastElementChild);
|
||||
|
||||
this.setState({
|
||||
canScrollPrevious: hasHiddenCardToLeft,
|
||||
canScrollNext: hasHiddenCardToRight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll(scrollTarget: number) {
|
||||
const scrollWrapper = this.scrollWrapper.current;
|
||||
if (scrollWrapper) {
|
||||
const currentScrollLeft = scrollWrapper.scrollLeft;
|
||||
const direction = currentScrollLeft > scrollTarget ? 'left' : 'right';
|
||||
this.scrollCardsAnimated(scrollWrapper, scrollTarget, direction);
|
||||
}
|
||||
}
|
||||
|
||||
scrollCardsAnimated = (scrollWrapper: HTMLUListElement, scrollTarget: number, direction: string) => {
|
||||
let start;
|
||||
const step = timestamp => {
|
||||
if (!start) start = timestamp;
|
||||
|
||||
const currentLeftVal = scrollWrapper.scrollLeft;
|
||||
|
||||
let newTarget;
|
||||
let shouldContinue;
|
||||
let progress = currentLeftVal;
|
||||
|
||||
if (direction === 'right') {
|
||||
progress += timestamp - start;
|
||||
newTarget = Math.min(progress, scrollTarget);
|
||||
shouldContinue = newTarget < scrollTarget;
|
||||
} else {
|
||||
progress -= timestamp - start;
|
||||
newTarget = Math.max(progress, scrollTarget);
|
||||
shouldContinue = newTarget > scrollTarget;
|
||||
}
|
||||
|
||||
scrollWrapper.scrollLeft = newTarget;
|
||||
|
||||
if (shouldContinue) {
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
// check if a card is fully visible horizontally
|
||||
isCardVisible = (card: HTMLLIElement): boolean => {
|
||||
if (!card) {
|
||||
return false;
|
||||
}
|
||||
const scrollWrapper = this.scrollWrapper.current;
|
||||
if (scrollWrapper) {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const isVisible = scrollWrapper.scrollLeft < card.offsetLeft && rect.left >= 0 && rect.right <= window.innerWidth;
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
handleScrollNext() {
|
||||
const scrollWrapper = this.scrollWrapper.current;
|
||||
if (!scrollWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = scrollWrapper.getElementsByTagName('li');
|
||||
|
||||
// Loop over items until we find one that is visible
|
||||
// The card before that (starting from the end) is the new "first" card on the screen
|
||||
|
||||
let previousCard: ?HTMLLIElement;
|
||||
for (let i = cards.length - 1; i > 0; i -= 1) {
|
||||
const currentCard: HTMLLIElement = cards[i];
|
||||
const currentCardVisible = this.isCardVisible(currentCard);
|
||||
|
||||
if (currentCardVisible && previousCard) {
|
||||
const scrollTarget = previousCard.offsetLeft;
|
||||
this.handleScroll(scrollTarget - cards[0].offsetLeft);
|
||||
break;
|
||||
}
|
||||
|
||||
previousCard = currentCard;
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollPrevious() {
|
||||
const scrollWrapper = this.scrollWrapper.current;
|
||||
if (!scrollWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = scrollWrapper.getElementsByTagName('li');
|
||||
|
||||
let hasFoundCard;
|
||||
let numberOfCardsThatCanFit = 0;
|
||||
|
||||
// loop starting at the end until we find a visible card
|
||||
// then count to find how many cards can fit on the screen
|
||||
for (let i = cards.length - 1; i >= 0; i -= 1) {
|
||||
const currentCard = cards[i];
|
||||
const isCurrentCardVisible = this.isCardVisible(currentCard);
|
||||
|
||||
if (isCurrentCardVisible) {
|
||||
if (!hasFoundCard) {
|
||||
hasFoundCard = true;
|
||||
}
|
||||
|
||||
numberOfCardsThatCanFit += 1;
|
||||
} else if (hasFoundCard) {
|
||||
// this card is off the screen to the left
|
||||
// we know how many cards can fit on a screen
|
||||
// find the new target and scroll
|
||||
const firstCardOffsetLeft = cards[0].offsetLeft;
|
||||
const cardIndexToScrollTo = i + 1 - numberOfCardsThatCanFit;
|
||||
const newFirstCard = cards[cardIndexToScrollTo];
|
||||
|
||||
let scrollTarget;
|
||||
if (newFirstCard) {
|
||||
scrollTarget = newFirstCard.offsetLeft;
|
||||
} else {
|
||||
// more cards can fit on the screen than are currently hidden
|
||||
// just scroll to the first card
|
||||
scrollTarget = cards[0].offsetLeft;
|
||||
}
|
||||
|
||||
scrollTarget -= firstCardOffsetLeft; // to play nice with the margins
|
||||
|
||||
this.handleScroll(scrollTarget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { category, categoryLink, urisInList, obscureNsfw, lazyLoad } = this.props;
|
||||
const { canScrollNext, canScrollPrevious } = this.state;
|
||||
const isCommunityTopBids = category.match(/^community/i);
|
||||
const showScrollButtons = isCommunityTopBids ? !obscureNsfw : true;
|
||||
|
||||
let channelLink;
|
||||
if (categoryLink) {
|
||||
channelLink = formatLbryUriForWeb(categoryLink);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="media-group--row">
|
||||
<header className="media-group__header">
|
||||
<h2 className="media-group__header-title">
|
||||
{categoryLink ? (
|
||||
<React.Fragment>
|
||||
<Button label={category} navigate={channelLink} />
|
||||
<SubscribeButton button="alt" showSnackBarOnSubscribe uri={`lbry://${categoryLink}`} />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<span>{category}</span>
|
||||
)}
|
||||
{isCommunityTopBids && (
|
||||
<ToolTip
|
||||
direction="top"
|
||||
label={__("What's this?")}
|
||||
body={__(
|
||||
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names from "one" to "ten" to put your content here!'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
{showScrollButtons && (
|
||||
<nav className="media-group__header-navigation">
|
||||
<Button disabled={!canScrollPrevious} onClick={this.handleScrollPrevious} icon={ICONS.ARROW_LEFT} />
|
||||
<Button disabled={!canScrollNext} onClick={this.handleScrollNext} icon={ICONS.ARROW_RIGHT} />
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
{obscureNsfw && isCommunityTopBids ? (
|
||||
<p className="media__message help--warning">
|
||||
{__(
|
||||
'The community top bids section is only visible if you allow mature content in the app. You can change your content viewing preferences'
|
||||
)}{' '}
|
||||
<Button button="link" navigate="/$/settings" label={__('here')} />.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="media-scrollhouse" ref={this.scrollWrapper}>
|
||||
{urisInList &&
|
||||
urisInList.map(uri => (
|
||||
<FileCard placeholder preventResolve={lazyLoad} showSubscribedLogo key={uri} uri={normalizeURI(uri)} />
|
||||
))}
|
||||
|
||||
{!urisInList && new Array(10).fill(1).map((x, i) => <FileCard placeholder key={i} />)}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryList;
|
|
@ -22,8 +22,8 @@ function ChannelContent(props: Props) {
|
|||
const showAbout = description || email || website;
|
||||
|
||||
return (
|
||||
<section>
|
||||
{!showAbout && <h2 className="empty">{__('Nothing here yet')}</h2>}
|
||||
<section className="card--section">
|
||||
{!showAbout && <h2 className="main--empty empty">{__('Nothing here yet')}</h2>}
|
||||
{showAbout && (
|
||||
<Fragment>
|
||||
{description && (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React, { Fragment } from 'react';
|
||||
import FileList from 'component/fileList';
|
||||
import ClaimList from 'component/claimList';
|
||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Paginate from 'component/common/paginate';
|
||||
|
@ -19,7 +19,6 @@ type Props = {
|
|||
function ChannelContent(props: Props) {
|
||||
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props;
|
||||
const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{fetching && !hasContent && (
|
||||
|
@ -28,11 +27,15 @@ function ChannelContent(props: Props) {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{!fetching && !hasContent && <h2 className="empty">{__("This channel hasn't uploaded anything.")}</h2>}
|
||||
{!fetching && !hasContent && (
|
||||
<div className="card--section">
|
||||
<h2 className="card__content help">{__("This channel hasn't uploaded anything.")}</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
||||
|
||||
{hasContent && <FileList sortByHeight hideFilter fileInfos={claimsInChannel} />}
|
||||
{hasContent && <ClaimList header={false} uris={claimsInChannel.map(claim => claim.permanent_url)} />}
|
||||
|
||||
<Paginate
|
||||
onPageChange={page => fetchClaims(uri, page)}
|
||||
|
|
|
@ -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);
|
11
src/ui/component/claimList/index.js
Normal file
11
src/ui/component/claimList/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ClaimList from './view';
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ClaimList);
|
70
src/ui/component/claimList/view.jsx
Normal file
70
src/ui/component/claimList/view.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import ClaimListItem from 'component/claimListItem';
|
||||
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 = {
|
||||
uris: Array<string>,
|
||||
header: Node | boolean,
|
||||
headerAltControls: Node,
|
||||
injectedItem?: Node,
|
||||
loading: boolean,
|
||||
type: string,
|
||||
empty?: string,
|
||||
meta?: Node,
|
||||
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
||||
persistedStorageKey?: string,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
const { uris, headerAltControls, injectedItem, loading, persistedStorageKey, empty, meta, type, header } = props;
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
const sortedUris = uris && currentSort === SORT_OLD ? uris.reverse() : uris;
|
||||
const hasUris = uris && !!uris.length;
|
||||
|
||||
function handleSortChange() {
|
||||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classnames('file-list')}>
|
||||
{header !== false && (
|
||||
<div className={classnames('file-list__header', { 'file-list__header--small': type === 'small' })}>
|
||||
{header || (
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
type="select"
|
||||
name="file_sort"
|
||||
value={currentSort}
|
||||
onChange={handleSortChange}
|
||||
>
|
||||
<option value={SORT_NEW}>{__('Newest First')}</option>
|
||||
<option value={SORT_OLD}>{__('Oldest First')}</option>
|
||||
</FormField>
|
||||
)}
|
||||
{loading && <Spinner light type="small" />}
|
||||
<div className="file-list__alt-controls">{headerAltControls}</div>
|
||||
</div>
|
||||
)}
|
||||
{meta && <div className="file-list__meta">{meta}</div>}
|
||||
{hasUris && (
|
||||
<ul>
|
||||
{sortedUris.map((uri, index) => (
|
||||
<React.Fragment key={uri}>
|
||||
<ClaimListItem uri={uri} type={type} />
|
||||
{index === 4 && injectedItem && <li className="file-list__item--injected">{injectedItem}</li>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{!hasUris && !loading && <h2 className="main--empty empty">{empty || __('No results')}</h2>}
|
||||
</section>
|
||||
);
|
||||
}
|
18
src/ui/component/claimListDiscover/index.js
Normal file
18
src/ui/component/claimListDiscover/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doClaimSearch, selectLastClaimSearchUris, selectFetchingClaimSearch, doToggleTagFollow } from 'lbry-redux';
|
||||
import ClaimListDiscover from './view';
|
||||
|
||||
const select = state => ({
|
||||
uris: selectLastClaimSearchUris(state),
|
||||
loading: selectFetchingClaimSearch(state),
|
||||
});
|
||||
|
||||
const perform = {
|
||||
doClaimSearch,
|
||||
doToggleTagFollow,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ClaimListDiscover);
|
140
src/ui/component/claimListDiscover/view.jsx
Normal file
140
src/ui/component/claimListDiscover/view.jsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import moment from 'moment';
|
||||
import { FormField } from 'component/common/form';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Tag from 'component/tag';
|
||||
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 SEARCH_SORT_YOU = 'you';
|
||||
const SEARCH_SORT_ALL = 'everyone';
|
||||
const TYPE_TRENDING = 'trending';
|
||||
const TYPE_TOP = 'top';
|
||||
const TYPE_NEW = 'new';
|
||||
const SEARCH_FILTER_TYPES = [SEARCH_SORT_YOU, SEARCH_SORT_ALL];
|
||||
const SEARCH_TYPES = ['trending', 'top', 'new'];
|
||||
const SEARCH_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,
|
||||
doToggleTagFollow: string => void,
|
||||
meta?: Node,
|
||||
};
|
||||
|
||||
function ClaimListDiscover(props: Props) {
|
||||
const { doClaimSearch, uris, tags, loading, personal, injectedItem, meta } = props;
|
||||
const [personalSort, setPersonalSort] = usePersistedState('file-list-trending:personalSort', SEARCH_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 ((newTags && !personal) || (newTags && personal && personalSort === SEARCH_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);
|
||||
}, [personal, personalSort, typeSort, timeSort, doClaimSearch, tagsString]);
|
||||
|
||||
const header = (
|
||||
<h1 className="card__title--flex">
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
type="select"
|
||||
name="trending_sort"
|
||||
value={typeSort}
|
||||
onChange={e => setTypeSort(e.target.value)}
|
||||
>
|
||||
{SEARCH_TYPES.map(type => (
|
||||
<option key={type} value={type}>
|
||||
{toCapitalCase(type)}
|
||||
</option>
|
||||
))}
|
||||
</FormField>
|
||||
<span>{__('For')}</span>
|
||||
{!personal && tags && tags.length ? (
|
||||
tags.map(tag => <Tag key={tag} name={tag} disabled />)
|
||||
) : (
|
||||
<FormField
|
||||
type="select"
|
||||
name="trending_overview"
|
||||
className="file-list__dropdown"
|
||||
value={personalSort}
|
||||
onChange={e => setPersonalSort(e.target.value)}
|
||||
>
|
||||
{SEARCH_FILTER_TYPES.map(type => (
|
||||
<option key={type} value={type}>
|
||||
{toCapitalCase(type)}
|
||||
</option>
|
||||
))}
|
||||
</FormField>
|
||||
)}
|
||||
</h1>
|
||||
);
|
||||
|
||||
const headerAltControls = (
|
||||
<React.Fragment>
|
||||
{typeSort === 'top' && (
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
type="select"
|
||||
name="trending_time"
|
||||
value={timeSort}
|
||||
onChange={e => setTimeSort(e.target.value)}
|
||||
>
|
||||
{SEARCH_TIMES.map(time => (
|
||||
<option key={time} value={time}>
|
||||
{toCapitalCase(time)}
|
||||
</option>
|
||||
))}
|
||||
</FormField>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<ClaimList
|
||||
meta={meta}
|
||||
loading={loading}
|
||||
uris={uris}
|
||||
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
|
||||
header={header}
|
||||
headerAltControls={headerAltControls}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimListDiscover;
|
|
@ -2,8 +2,6 @@ import { connect } from 'react-redux';
|
|||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimIsPending,
|
||||
|
@ -11,25 +9,15 @@ import {
|
|||
makeSelectTitleForUri,
|
||||
makeSelectClaimIsNsfw,
|
||||
} from 'lbry-redux';
|
||||
import { selectRewardContentClaimIds } from 'lbryinc';
|
||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
||||
import { doClearContentHistoryUri } from 'redux/actions/content';
|
||||
import FileCard from './view';
|
||||
import ClaimListItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
position: makeSelectContentPositionForUri(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
isNew: makeSelectIsNew(props.uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
|
@ -37,10 +25,9 @@ const select = (state, props) => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileCard);
|
||||
)(ClaimListItem);
|
130
src/ui/component/claimListItem/view.jsx
Normal file
130
src/ui/component/claimListItem/view.jsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
// @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 ClaimTags from 'component/claimTags';
|
||||
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,
|
||||
placeholder: boolean,
|
||||
type: string,
|
||||
};
|
||||
|
||||
function ClaimListItem(props: Props) {
|
||||
const {
|
||||
obscureNsfw,
|
||||
claimIsMine,
|
||||
pending,
|
||||
history,
|
||||
uri,
|
||||
isResolvingUri,
|
||||
thumbnail,
|
||||
title,
|
||||
nsfw,
|
||||
resolveUri,
|
||||
claim,
|
||||
placeholder,
|
||||
type,
|
||||
} = 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': type === '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>
|
||||
{type !== 'small' && (
|
||||
<div>
|
||||
{isChannel && <SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />}
|
||||
{!isChannel && <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>
|
||||
|
||||
<ClaimTags uri={uri} type={type} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(ClaimListItem);
|
13
src/ui/component/claimTags/index.js
Normal file
13
src/ui/component/claimTags/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectTagsForUri, selectFollowedTags } from 'lbry-redux';
|
||||
import ClaimTags from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
tags: makeSelectTagsForUri(props.uri)(state),
|
||||
followedTags: selectFollowedTags(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(ClaimTags);
|
52
src/ui/component/claimTags/view.jsx
Normal file
52
src/ui/component/claimTags/view.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
|
||||
const SLIM_TAGS = 2;
|
||||
const NORMAL_TAGS = 4;
|
||||
const LARGE_TAGS = 10;
|
||||
|
||||
type Props = {
|
||||
tags: Array<string>,
|
||||
followedTags: Array<Tag>,
|
||||
type: string,
|
||||
};
|
||||
|
||||
export default function ClaimTags(props: Props) {
|
||||
const { tags, followedTags, type } = props;
|
||||
const numberOfTags = type === 'small' ? SLIM_TAGS : type === 'large' ? LARGE_TAGS : NORMAL_TAGS;
|
||||
|
||||
let tagsToDisplay = [];
|
||||
for (var i = 0; tagsToDisplay.length < numberOfTags - 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 === numberOfTags) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!tagsToDisplay.includes(tag)) {
|
||||
tagsToDisplay.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames('file-properties', { 'file-properties--large': type === 'large' })}>
|
||||
{tagsToDisplay.map(tag => (
|
||||
<Button key={tag} title={tag} navigate={`$/tags?t=${tag}`} className="tag" label={tag} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -10,7 +10,6 @@ type Props = {
|
|||
showFullPrice: boolean,
|
||||
showPlus: boolean,
|
||||
isEstimate?: boolean,
|
||||
large?: boolean,
|
||||
showLBC?: boolean,
|
||||
fee?: boolean,
|
||||
badge?: boolean,
|
||||
|
@ -27,7 +26,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { amount, precision, showFullPrice, showFree, showPlus, large, isEstimate, fee, showLBC, badge } = this.props;
|
||||
const { amount, precision, showFullPrice, showFree, showPlus, isEstimate, fee, showLBC, badge } = this.props;
|
||||
|
||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||
const fullPrice = formatFullPrice(amount, 2);
|
||||
|
@ -69,7 +68,6 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
badge,
|
||||
'badge--cost': badge && amount > 0,
|
||||
'badge--free': badge && isFree,
|
||||
'badge--large': large,
|
||||
})}
|
||||
>
|
||||
{amountText}
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class CopyableText extends React.PureComponent<Props> {
|
|||
onFocus={this.onFocus}
|
||||
inputButton={
|
||||
<Button
|
||||
button="primary"
|
||||
button="inverse"
|
||||
icon={ICONS.CLIPBOARD}
|
||||
onClick={() => {
|
||||
clipboard.writeText(copyable);
|
||||
|
|
|
@ -12,10 +12,6 @@ const select = state => ({
|
|||
});
|
||||
|
||||
const perform = dispatch => () => ({
|
||||
completeFirstRun: () => {
|
||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
||||
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
|
||||
},
|
||||
acknowledgeEmail: () => {
|
||||
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as ICONS from 'constants/icons';
|
|||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import { requestFullscreen, fullscreenElement } from 'util/full-screen';
|
||||
|
||||
type FileInfo = {
|
||||
claim_id: string,
|
||||
|
@ -15,15 +16,32 @@ type Props = {
|
|||
openModal: (id: string, { uri: string }) => void,
|
||||
claimIsMine: boolean,
|
||||
fileInfo: FileInfo,
|
||||
viewerContainer: React.Ref,
|
||||
showFullscreen: boolean,
|
||||
};
|
||||
|
||||
class FileActions extends React.PureComponent<Props> {
|
||||
maximizeViewer = () => {
|
||||
const { viewerContainer } = this.props;
|
||||
const isFullscreen = fullscreenElement();
|
||||
// Request fullscreen if viewer is ready
|
||||
// And if there is no fullscreen element active
|
||||
if (!isFullscreen && viewerContainer && viewerContainer.current !== null) {
|
||||
requestFullscreen(viewerContainer.current);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props;
|
||||
const { fileInfo, uri, openModal, claimIsMine, claimId, showFullscreen } = this.props;
|
||||
const showDelete = claimIsMine || (fileInfo && Object.keys(fileInfo).length > 0);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showFullscreen && (
|
||||
<Tooltip onComponent body={__('Full screen (f)')}>
|
||||
<Button button="alt" description={__('Fullscreen')} icon={ICONS.FULLSCREEN} onClick={this.maximizeViewer} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{showDelete && (
|
||||
<Tooltip onComponent body={__('Delete this file')}>
|
||||
<Button
|
||||
|
|
|
@ -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,16 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimsById, doSetFileListSort } from 'lbry-redux';
|
||||
import FileList from './view';
|
||||
|
||||
const select = state => ({
|
||||
claimsById: selectClaimsById(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
setFileListSort: (page, value) => dispatch(doSetFileListSort(page, value)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileList);
|
|
@ -1,165 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { buildURI, SORT_OPTIONS } from 'lbry-redux';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import FileCard from 'component/fileCard';
|
||||
|
||||
type Props = {
|
||||
hideFilter: boolean,
|
||||
sortByHeight?: boolean,
|
||||
claimsById: Array<StreamClaim>,
|
||||
fileInfos: Array<FileListItem>,
|
||||
sortBy: string,
|
||||
page?: string,
|
||||
setFileListSort: (?string, string) => void,
|
||||
};
|
||||
|
||||
class FileList extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
hideFilter: false,
|
||||
sortBy: SORT_OPTIONS.DATE_NEW,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
|
||||
|
||||
this.sortFunctions = {
|
||||
[SORT_OPTIONS.DATE_NEW]: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.sort((fileInfo1, fileInfo2) => {
|
||||
if (fileInfo1.confirmations < 1) {
|
||||
return -1;
|
||||
} else if (fileInfo2.confirmations < 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 0;
|
||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||
? this.props.claimsById[fileInfo2.claim_id].height
|
||||
: 0;
|
||||
|
||||
if (height1 !== height2) {
|
||||
// flipped because heigher block height is newer
|
||||
return height2 - height1;
|
||||
}
|
||||
|
||||
if (fileInfo1.absolute_channel_position && fileInfo2.absolute_channel_position) {
|
||||
return fileInfo1.absolute_channel_position - fileInfo2.absolute_channel_position;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
: [...fileInfos].reverse(),
|
||||
[SORT_OPTIONS.DATE_OLD]: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 999999;
|
||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||
? this.props.claimsById[fileInfo2.claim_id].height
|
||||
: 999999;
|
||||
if (height1 < height2) {
|
||||
return -1;
|
||||
} else if (height1 > height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
: fileInfos,
|
||||
[SORT_OPTIONS.TITLE]: fileInfos =>
|
||||
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const getFileTitle = fileInfo => {
|
||||
const { value, name, claim_name: claimName } = fileInfo;
|
||||
if (value) {
|
||||
return value.title || claimName;
|
||||
}
|
||||
|
||||
// Invalid claim
|
||||
return '';
|
||||
};
|
||||
const title1 = getFileTitle(fileInfo1).toLowerCase();
|
||||
const title2 = getFileTitle(fileInfo2).toLowerCase();
|
||||
if (title1 < title2) {
|
||||
return -1;
|
||||
} else if (title1 > title2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
[SORT_OPTIONS.FILENAME]: fileInfos =>
|
||||
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
|
||||
const fileName1Lower = fileName1.toLowerCase();
|
||||
const fileName2Lower = fileName2.toLowerCase();
|
||||
if (fileName1Lower < fileName2Lower) {
|
||||
return -1;
|
||||
} else if (fileName2Lower > fileName1Lower) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
getChannelSignature = (fileInfo: { pending: boolean } & FileListItem) => {
|
||||
if (fileInfo.pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fileInfo.channel_claim_id;
|
||||
};
|
||||
|
||||
handleSortChanged(event: SyntheticInputEvent<*>) {
|
||||
this.props.setFileListSort(this.props.page, event.target.value);
|
||||
}
|
||||
|
||||
sortFunctions: {};
|
||||
|
||||
render() {
|
||||
const { fileInfos, hideFilter, sortBy } = this.props;
|
||||
|
||||
const content = [];
|
||||
if (!fileInfos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId, txid, nout, isNew } = fileInfo;
|
||||
const uriParams = {};
|
||||
|
||||
// This is unfortunate
|
||||
// https://github.com/lbryio/lbry/issues/1159
|
||||
const name = claimName || claimNameDownloaded;
|
||||
uriParams.contentName = name;
|
||||
uriParams.claimId = claimId;
|
||||
const uri = buildURI(uriParams);
|
||||
const outpoint = `${txid}:${nout}`;
|
||||
|
||||
// See https://github.com/lbryio/lbry-desktop/issues/1327 for discussion around using outpoint as the key
|
||||
content.push(<FileCard key={outpoint} uri={uri} isNew={isNew} />);
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
{!hideFilter && (
|
||||
<Form>
|
||||
<FormField label={__('Sort by')} type="select" value={sortBy} onChange={this.handleSortChanged}>
|
||||
<option value={SORT_OPTIONS.DATE_NEW}>{__('Newest First')}</option>
|
||||
<option value={SORT_OPTIONS.DATE_OLD}>{__('Oldest First')}</option>
|
||||
<option value={SORT_OPTIONS.TITLE}>{__('Title')}</option>
|
||||
</FormField>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<section className="media-group--list">
|
||||
<div className="card__list">{content}</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileList;
|
|
@ -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);
|
30
src/ui/component/fileProperties/view.jsx
Normal file
30
src/ui/component/fileProperties/view.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @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 } = 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} />}
|
||||
<FilePrice hideFree uri={uri} />
|
||||
</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,14 +1,20 @@
|
|||
// @flow
|
||||
import '@babel/polyfill';
|
||||
import * as React from 'react';
|
||||
|
||||
// @if TARGET='app'
|
||||
import { remote } from 'electron';
|
||||
import fs from 'fs';
|
||||
import { remote } from 'electron';
|
||||
// @endif
|
||||
|
||||
import path from 'path';
|
||||
import player from 'render-media';
|
||||
import FileRender from 'component/fileRender';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import { fullscreenElement, requestFullscreen, exitFullscreen } from 'util/full-screen';
|
||||
|
||||
// Shorcut key code for fullscreen (f)
|
||||
const F_KEYCODE = 70;
|
||||
|
||||
type Props = {
|
||||
contentType: string,
|
||||
|
@ -23,6 +29,8 @@ type Props = {
|
|||
onFinishCb: ?() => void,
|
||||
savePosition: number => void,
|
||||
changeVolume: number => void,
|
||||
viewerContainer: React.Ref,
|
||||
searchBarFocused: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -69,7 +77,6 @@ class MediaPlayer extends React.PureComponent<Props, State> {
|
|||
|
||||
this.mediaContainer = React.createRef();
|
||||
(this: any).togglePlay = this.togglePlay.bind(this);
|
||||
(this: any).toggleFullScreen = this.toggleFullScreen.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -107,25 +114,65 @@ class MediaPlayer extends React.PureComponent<Props, State> {
|
|||
componentWillUnmount() {
|
||||
const mediaElement = this.mediaContainer.current.children[0];
|
||||
|
||||
document.removeEventListener('keydown', this.togglePlay);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
if (mediaElement) {
|
||||
mediaElement.removeEventListener('click', this.togglePlay);
|
||||
mediaElement.removeEventListener('dbclick', this.handleDoubleClick);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFullScreen() {
|
||||
const mediaElement = this.mediaContainer.current;
|
||||
if (mediaElement) {
|
||||
// $FlowFixMe
|
||||
if (document.webkitIsFullScreen) {
|
||||
// $FlowFixMe
|
||||
document.webkitExitFullscreen();
|
||||
} else {
|
||||
mediaElement.webkitRequestFullScreen();
|
||||
handleKeyDown = (event: SyntheticKeyboardEvent<*>) => {
|
||||
const { searchBarFocused } = this.props;
|
||||
|
||||
if (!searchBarFocused) {
|
||||
// Handle fullscreen shortcut key (f)
|
||||
if (event.keyCode === F_KEYCODE) {
|
||||
this.toggleFullscreen();
|
||||
}
|
||||
// Handle toggle play
|
||||
// @if TARGET='app'
|
||||
this.togglePlay(event);
|
||||
// @endif
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleDoubleClick = (event: SyntheticInputEvent<*>) => {
|
||||
// Prevent pause / play
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// Trigger fullscreen mode
|
||||
this.toggleFullscreen();
|
||||
};
|
||||
|
||||
toggleFullscreen = () => {
|
||||
const { viewerContainer } = this.props;
|
||||
const isFullscreen = fullscreenElement();
|
||||
const isSupportedFile = this.isSupportedFile();
|
||||
const isPlayableType = this.playableType();
|
||||
|
||||
if (!isFullscreen) {
|
||||
// Enter fullscreen mode if content is not playable
|
||||
// Otherwise it should be handle internally on the video player
|
||||
// or it will break the toggle fullscreen button
|
||||
if (!isPlayableType && isSupportedFile && viewerContainer && viewerContainer.current !== null) {
|
||||
requestFullscreen(viewerContainer.current);
|
||||
}
|
||||
// Request fullscreen mode for the media player (renderMedia)
|
||||
// Don't use this with the new player
|
||||
// @if TARGET='app'
|
||||
else if (isPlayableType) {
|
||||
const mediaContainer = this.mediaContainer.current;
|
||||
const mediaElement = mediaContainer && mediaContainer.children[0];
|
||||
if (mediaElement) {
|
||||
requestFullscreen(mediaElement);
|
||||
}
|
||||
}
|
||||
// @endif
|
||||
} else {
|
||||
exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
async playMedia() {
|
||||
const container = this.mediaContainer.current;
|
||||
|
@ -183,7 +230,6 @@ class MediaPlayer extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.togglePlay);
|
||||
const mediaElement = container.children[0];
|
||||
if (mediaElement) {
|
||||
if (position) {
|
||||
|
@ -206,7 +252,7 @@ class MediaPlayer extends React.PureComponent<Props, State> {
|
|||
changeVolume(mediaElement.volume);
|
||||
});
|
||||
mediaElement.volume = volume;
|
||||
mediaElement.addEventListener('dblclick', this.toggleFullScreen);
|
||||
mediaElement.addEventListener('dblclick', this.handleDoubleClick);
|
||||
}
|
||||
// @endif
|
||||
|
||||
|
@ -217,6 +263,9 @@ class MediaPlayer extends React.PureComponent<Props, State> {
|
|||
this.renderFile();
|
||||
}
|
||||
// @endif
|
||||
|
||||
// Fullscreen event for web and app
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
// @if TARGET='app'
|
||||
|
|
|
@ -7,8 +7,10 @@ import LoadingScreen from 'component/common/loading-screen';
|
|||
import PlayButton from './internal/play-button';
|
||||
|
||||
const Player = React.lazy(() =>
|
||||
import(/* webpackChunkName: "player-legacy" */
|
||||
'./internal/player')
|
||||
import(
|
||||
/* webpackChunkName: "player-legacy" */
|
||||
'./internal/player'
|
||||
)
|
||||
);
|
||||
|
||||
const SPACE_BAR_KEYCODE = 32;
|
||||
|
@ -49,6 +51,8 @@ type Props = {
|
|||
insufficientCredits: boolean,
|
||||
nsfw: boolean,
|
||||
thumbnail: ?string,
|
||||
isPlayableType: boolean,
|
||||
viewerContainer: React.Ref,
|
||||
};
|
||||
|
||||
class FileViewer extends React.PureComponent<Props> {
|
||||
|
@ -125,9 +129,12 @@ class FileViewer extends React.PureComponent<Props> {
|
|||
|
||||
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
|
||||
const { searchBarFocused } = this.props;
|
||||
if (!searchBarFocused && event.keyCode === SPACE_BAR_KEYCODE) {
|
||||
event.preventDefault(); // prevent page scroll
|
||||
this.playContent();
|
||||
|
||||
if (!searchBarFocused) {
|
||||
if (event.keyCode === SPACE_BAR_KEYCODE) {
|
||||
event.preventDefault(); // prevent page scroll
|
||||
this.playContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,6 +229,8 @@ class FileViewer extends React.PureComponent<Props> {
|
|||
obscureNsfw,
|
||||
mediaType,
|
||||
insufficientCredits,
|
||||
viewerContainer,
|
||||
searchBarFocused,
|
||||
thumbnail,
|
||||
nsfw,
|
||||
} = this.props;
|
||||
|
@ -257,7 +266,7 @@ class FileViewer extends React.PureComponent<Props> {
|
|||
const layoverStyle = !shouldObscureNsfw && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {};
|
||||
|
||||
return (
|
||||
<div className={classnames('video', {}, className)}>
|
||||
<div className={classnames('video', {}, className)} ref={viewerContainer}>
|
||||
{isPlaying && (
|
||||
<div className="content__view">
|
||||
{!isReadyToPlay ? (
|
||||
|
@ -282,6 +291,8 @@ class FileViewer extends React.PureComponent<Props> {
|
|||
onStartCb={this.onFileStartCb}
|
||||
onFinishCb={this.onFileFinishCb}
|
||||
playingUri={playingUri}
|
||||
searchBarFocused={searchBarFocused}
|
||||
viewerContainer={viewerContainer}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,21 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import { selectBalance, SETTINGS as LBRY_REDUX_SETTINGS } from 'lbry-redux';
|
||||
import { formatCredits } from 'util/format-credits';
|
||||
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
|
||||
import { doDownloadUpgradeRequested } from 'redux/actions/app';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import Header from './view';
|
||||
|
||||
const select = state => ({
|
||||
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
|
||||
balance: selectBalance(state),
|
||||
isUpgradeAvailable: selectIsUpgradeAvailable(state),
|
||||
language: makeSelectClientSetting(LBRY_REDUX_SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
||||
roundedBalance: formatCredits(selectBalance(state) || 0, 2),
|
||||
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
|
||||
automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
downloadUpgradeRequested: () => dispatch(doDownloadUpgradeRequested()),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -1,116 +1,124 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import * as React from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import Button from 'component/button';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import WunderBar from 'component/wunderbar';
|
||||
import Icon from 'component/common/icon';
|
||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
|
||||
type Props = {
|
||||
autoUpdateDownloaded: boolean,
|
||||
balance: string,
|
||||
isUpgradeAvailable: boolean,
|
||||
roundedBalance: string,
|
||||
isBackDisabled: boolean,
|
||||
isForwardDisabled: boolean,
|
||||
back: () => void,
|
||||
forward: () => void,
|
||||
roundedBalance: number,
|
||||
downloadUpgradeRequested: any => void,
|
||||
history: { push: string => void },
|
||||
currentTheme: string,
|
||||
automaticDarkModeEnabled: boolean,
|
||||
setClientSetting: (string, boolean | string) => void,
|
||||
};
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const {
|
||||
autoUpdateDownloaded,
|
||||
balance,
|
||||
downloadUpgradeRequested,
|
||||
isUpgradeAvailable,
|
||||
roundedBalance,
|
||||
back,
|
||||
isBackDisabled,
|
||||
forward,
|
||||
isForwardDisabled,
|
||||
} = props;
|
||||
const { roundedBalance, history, setClientSetting, currentTheme, automaticDarkModeEnabled } = props;
|
||||
|
||||
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
|
||||
function handleThemeToggle() {
|
||||
if (automaticDarkModeEnabled) {
|
||||
setClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false);
|
||||
}
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
setClientSetting(SETTINGS.THEME, 'light');
|
||||
} else {
|
||||
setClientSetting(SETTINGS.THEME, 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--lbry"
|
||||
label={__('LBRY')}
|
||||
iconRight={ICONS.LBRY}
|
||||
navigate="/"
|
||||
/>
|
||||
{/* @if TARGET='app' */}
|
||||
<div className="header__navigation-arrows">
|
||||
<div className="header__contents">
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--back"
|
||||
description={__('Navigate back')}
|
||||
onClick={() => window.history.back()}
|
||||
icon={ICONS.ARROW_LEFT}
|
||||
iconSize={15}
|
||||
className="header__navigation-item header__navigation-item--lbry"
|
||||
label={__('LBRY')}
|
||||
icon={ICONS.LBRY}
|
||||
navigate="/"
|
||||
/>
|
||||
{/* @if TARGET='app' */}
|
||||
<div className="header__navigation-arrows">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--back"
|
||||
description={__('Navigate back')}
|
||||
onClick={() => window.history.back()}
|
||||
icon={ICONS.ARROW_LEFT}
|
||||
iconSize={15}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--forward"
|
||||
description={__('Navigate forward')}
|
||||
onClick={() => window.history.forward()}
|
||||
icon={ICONS.ARROW_RIGHT}
|
||||
iconSize={15}
|
||||
/>
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--forward"
|
||||
description={__('Navigate forward')}
|
||||
onClick={() => window.history.forward()}
|
||||
icon={ICONS.ARROW_RIGHT}
|
||||
iconSize={15}
|
||||
/>
|
||||
</div>
|
||||
{/* @endif */}
|
||||
</div>
|
||||
{/* @endif */}
|
||||
</div>
|
||||
|
||||
<WunderBar />
|
||||
<WunderBar />
|
||||
|
||||
<div className="header__navigation">
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--menu"
|
||||
description={__('Menu')}
|
||||
icon={ICONS.MENU}
|
||||
iconSize={15}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
description={__('Your wallet')}
|
||||
title={`Your balance is ${balance} LBRY Credits`}
|
||||
label={
|
||||
<React.Fragment>
|
||||
{roundedBalance} <LbcSymbol />
|
||||
</React.Fragment>
|
||||
}
|
||||
navigate="/$/account"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action"
|
||||
activeClass="header__navigation-item--active"
|
||||
description={__('Publish content')}
|
||||
icon={ICONS.UPLOAD}
|
||||
iconSize={24}
|
||||
label={isUpgradeAvailable ? '' : __('Publish')}
|
||||
navigate="/$/publish"
|
||||
/>
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
|
||||
{showUpgradeButton && (
|
||||
<Button
|
||||
className="header__navigation-item header__navigation-item--right-action header__navigation-item--upgrade"
|
||||
icon={ICONS.DOWNLOAD}
|
||||
iconSize={24}
|
||||
label={__('Upgrade App')}
|
||||
onClick={downloadUpgradeRequested}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
<div className="header__navigation">
|
||||
<Menu>
|
||||
<MenuButton className="header__navigation-item menu__title">
|
||||
<Icon icon={ICONS.ACCOUNT} />
|
||||
{roundedBalance > 0 ? (
|
||||
<React.Fragment>
|
||||
{roundedBalance} <LbcSymbol />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
__('Account')
|
||||
)}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/account`)}>
|
||||
<Icon aria-hidden icon={ICONS.OVERVIEW} />
|
||||
{__('Overview')}
|
||||
</MenuItem>
|
||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/wallet`)}>
|
||||
<Icon aria-hidden icon={ICONS.WALLET} />
|
||||
{__('Wallet')}
|
||||
</MenuItem>
|
||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/publish`)}>
|
||||
<Icon aria-hidden icon={ICONS.UPLOAD} />
|
||||
{__('Publish')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Menu>
|
||||
<MenuButton className="header__navigation-item menu__title">
|
||||
<Icon icon={ICONS.SETTINGS} />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/settings`)}>
|
||||
<Icon aria-hidden icon={ICONS.SETTINGS} />
|
||||
{__('Settings')}
|
||||
</MenuItem>
|
||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/help`)}>
|
||||
<Icon aria-hidden icon={ICONS.HELP} />
|
||||
{__('Help')}
|
||||
</MenuItem>
|
||||
<MenuItem className="menu__link" onSelect={handleThemeToggle}>
|
||||
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
|
||||
{currentTheme === 'light' ? 'Dark' : 'Light'}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default withRouter(Header);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { Form, FormField, Submit } from 'component/common/form';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import CopyableText from 'component/copyableText';
|
||||
|
||||
type FormProps = {
|
||||
|
@ -48,7 +48,7 @@ class FormInviteNew extends React.PureComponent<FormProps, FormState> {
|
|||
name="email"
|
||||
value={this.state.email}
|
||||
error={errorMessage}
|
||||
inputButton={<Submit label="Invite" disabled={isPending} />}
|
||||
inputButton={<Button button="inverse" type="submit" label="Invite" disabled={isPending} />}
|
||||
onChange={event => {
|
||||
this.handleEmailChanged(event);
|
||||
}}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function NavigationHistoryRecent(props: Props) {
|
|||
))}
|
||||
</section>
|
||||
<div className="card__actions">
|
||||
<Button navigate="/$/history/all" button="link" label={__('See All Visited Links')} />
|
||||
<Button navigate="/$/library/all" button="link" label={__('See All Visited Links')} />
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
@ -1,2 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
|
||||
import { doDownloadUpgradeRequested } from 'redux/actions/app';
|
||||
import Page from './view';
|
||||
export default Page;
|
||||
|
||||
const select = state => ({
|
||||
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
|
||||
isUpgradeAvailable: selectIsUpgradeAvailable(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
{
|
||||
doDownloadUpgradeRequested,
|
||||
}
|
||||
)(Page);
|
||||
|
|
|
@ -1,93 +1,34 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
// time in ms to wait to show loading spinner
|
||||
const LOADER_TIMEOUT = 1000;
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
children: React.Node | Array<React.Node>,
|
||||
pageTitle: ?string,
|
||||
notContained: ?boolean, // No max-width, but keep the padding
|
||||
loading: ?boolean,
|
||||
className: ?string,
|
||||
autoUpdateDownloaded: boolean,
|
||||
isUpgradeAvailable: boolean,
|
||||
doDownloadUpgradeRequested: () => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
showLoader: ?boolean,
|
||||
};
|
||||
function Page(props: Props) {
|
||||
const { children, className, autoUpdateDownloaded, isUpgradeAvailable, doDownloadUpgradeRequested } = props;
|
||||
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
|
||||
|
||||
class Page extends React.PureComponent<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
showLoader: false,
|
||||
};
|
||||
|
||||
this.loaderTimeout = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { loading } = this.props;
|
||||
if (loading) {
|
||||
this.beginLoadingTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { loading } = this.props;
|
||||
const { showLoader } = this.state;
|
||||
|
||||
if (!this.loaderTimeout && !prevProps.loading && loading) {
|
||||
this.beginLoadingTimeout();
|
||||
} else if (!loading && this.loaderTimeout) {
|
||||
clearTimeout(this.loaderTimeout);
|
||||
if (showLoader) {
|
||||
this.removeLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.loaderTimeout) {
|
||||
clearTimeout(this.loaderTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
beginLoadingTimeout() {
|
||||
this.loaderTimeout = setTimeout(() => {
|
||||
this.setState({ showLoader: true });
|
||||
}, LOADER_TIMEOUT);
|
||||
}
|
||||
|
||||
removeLoader() {
|
||||
this.setState({ showLoader: false });
|
||||
}
|
||||
|
||||
loaderTimeout: ?TimeoutID;
|
||||
|
||||
render() {
|
||||
const { children, notContained, loading, className } = this.props;
|
||||
const { showLoader } = this.state;
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classnames('main', className, {
|
||||
'main--contained': !notContained,
|
||||
'main--not-contained': notContained,
|
||||
})}
|
||||
>
|
||||
{!loading && children}
|
||||
{showLoader && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<main className={classnames('main', className)}>
|
||||
{/* @if TARGET='app' */}
|
||||
{showUpgradeButton && (
|
||||
<div className="main__status">
|
||||
{__('Update ready to install')}
|
||||
<Button button="alt" icon={ICONS.DOWNLOAD} label={__('Install now')} onClick={doDownloadUpgradeRequested} />
|
||||
</div>
|
||||
)}
|
||||
{/* @endif */}
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -394,15 +394,16 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
<header className="card__header">
|
||||
<h2 className="card__title">{__('Thumbnail')}</h2>
|
||||
<p className="card__subtitle">
|
||||
{uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
|
||||
__('Enter a URL for your thumbnail.')
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{__('Upload your thumbnail (.png/.jpg/.jpeg/.gif) to')}{' '}
|
||||
<Button button="link" label={__('spee.ch')} href="https://spee.ch/about" />.{' '}
|
||||
{__('Recommended size: 800x450 (16:9)')}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{(uploadThumbnailStatus === undefined && __('You should reselect your file to choose a thumbnail')) ||
|
||||
(uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
|
||||
__('Enter a URL for your thumbnail.')
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{__('Upload your thumbnail (.png/.jpg/.jpeg/.gif) to')}{' '}
|
||||
<Button button="link" label={__('spee.ch')} href="https://spee.ch/about" />.{' '}
|
||||
{__('Recommended size: 800x450 (16:9)')}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import FileTile from 'component/fileTile';
|
||||
import ClaimList from 'component/claimList';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -51,15 +51,14 @@ export default class RecommendedContent extends React.PureComponent<Props> {
|
|||
const { recommendedContent, isSearching } = this.props;
|
||||
|
||||
return (
|
||||
<section className="media-group--list-recommended">
|
||||
<span>Related</span>
|
||||
{recommendedContent &&
|
||||
recommendedContent.map(recommendedUri => (
|
||||
<FileTile hideNoResult size="small" key={recommendedUri} uri={recommendedUri} />
|
||||
))}
|
||||
{recommendedContent && !recommendedContent.length && !isSearching && (
|
||||
<div className="media__subtitle">No related content found</div>
|
||||
)}
|
||||
<section className="card">
|
||||
<ClaimList
|
||||
type="small"
|
||||
loading={isSearching}
|
||||
uris={recommendedContent}
|
||||
header={<span>{__('Related')}</span>}
|
||||
empty={<div className="empty">{__('No related content found')}</div>}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const RewardLink = (props: Props) => {
|
|||
const { reward, claimReward, label, isPending, button } = props;
|
||||
return !reward ? null : (
|
||||
<Button
|
||||
button={button ? 'primary' : 'link'}
|
||||
button={button ? 'inverse' : 'link'}
|
||||
disabled={isPending}
|
||||
label={isPending ? __('Claiming...') : label || `${__('Get')} ${reward.reward_amount} LBC`}
|
||||
onClick={() => {
|
||||
|
|
|
@ -23,8 +23,8 @@ const RewardListClaimed = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<section className="card">
|
||||
<header className="table__header">
|
||||
<h2 className="card__title">Claimed Rewards</h2>
|
||||
|
||||
<p className="card__subtitle">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectUnclaimedRewardValue, selectFetchingRewards, doRewardList, doFetchRewardedContent } from 'lbryinc';
|
||||
import { selectUnclaimedRewardValue, selectFetchingRewards, doFetchRewardedContent } from 'lbryinc';
|
||||
import RewardSummary from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
@ -8,7 +8,6 @@ const select = state => ({
|
|||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
});
|
||||
|
||||
|
|
|
@ -2,51 +2,38 @@
|
|||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
|
||||
type Props = {
|
||||
unclaimedRewardAmount: number,
|
||||
fetching: boolean,
|
||||
fetchRewards: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
};
|
||||
|
||||
class RewardSummary extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchRewards();
|
||||
this.props.fetchRewardedContent();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { unclaimedRewardAmount, fetching } = this.props;
|
||||
const hasRewards = unclaimedRewardAmount > 0;
|
||||
|
||||
return (
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">
|
||||
{__('Rewards')}
|
||||
{fetching && <BusyIndicator />}
|
||||
</h2>
|
||||
|
||||
<p className="card__subtitle">
|
||||
{!fetching &&
|
||||
(hasRewards ? (
|
||||
<React.Fragment>
|
||||
{__('You have')}
|
||||
|
||||
<CreditAmount inheritStyle amount={unclaimedRewardAmount} precision={8} />
|
||||
|
||||
{__('in unclaimed rewards')}.
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{__('There are no rewards available at this time, please check back later')}.
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
<h2 className="card__title">{__('Rewards')}</h2>
|
||||
</header>
|
||||
|
||||
<p className="card__subtitle">
|
||||
{fetching && __('You have...')}
|
||||
{!fetching && hasRewards ? (
|
||||
<React.Fragment>
|
||||
{/* @i18nfixme */}
|
||||
{__('You have')}
|
||||
|
||||
<CreditAmount inheritStyle amount={unclaimedRewardAmount} precision={8} />
|
||||
|
||||
{__('in unclaimed rewards')}.
|
||||
</React.Fragment>
|
||||
) : (
|
||||
__('You have no rewards available, please check')
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="card__content">
|
||||
<div className="card__actions">
|
||||
<Button
|
||||
|
@ -54,12 +41,8 @@ class RewardSummary extends React.Component<Props> {
|
|||
navigate="/$/rewards"
|
||||
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
|
||||
/>
|
||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />
|
||||
</div>
|
||||
|
||||
<p className="help">
|
||||
{__('Read our')} <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/rewards" />{' '}
|
||||
{__('to learn more about LBRY Rewards')}.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -33,10 +33,10 @@ const RewardTile = (props: Props) => {
|
|||
<div className="card__content">
|
||||
<div className="card__actions">
|
||||
{reward.reward_type === rewards.TYPE_GENERATED_CODE && (
|
||||
<Button button="primary" onClick={openRewardCodeModal} label={__('Enter Code')} />
|
||||
<Button button="inverse" onClick={openRewardCodeModal} label={__('Enter Code')} />
|
||||
)}
|
||||
{reward.reward_type === rewards.TYPE_REFERRAL && (
|
||||
<Button button="primary" navigate="/$/invite" label={__('Go To Invites')} />
|
||||
<Button button="inverse" navigate="/$/invite" label={__('Go To Invites')} />
|
||||
)}
|
||||
{reward.reward_type !== rewards.TYPE_REFERRAL &&
|
||||
(claimed ? (
|
||||
|
|
25
src/ui/component/rewardTotal/index.js
Normal file
25
src/ui/component/rewardTotal/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectUnclaimedRewardValue,
|
||||
selectFetchingRewards,
|
||||
doRewardList,
|
||||
doFetchRewardedContent,
|
||||
selectClaimedRewards,
|
||||
} from 'lbryinc';
|
||||
import RewardSummary from './view';
|
||||
|
||||
const select = state => ({
|
||||
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||
fetching: selectFetchingRewards(state),
|
||||
rewards: selectClaimedRewards(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(RewardSummary);
|
BIN
src/ui/component/rewardTotal/total-background.png
Normal file
BIN
src/ui/component/rewardTotal/total-background.png
Normal file
Binary file not shown.
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
25
src/ui/component/rewardTotal/view.jsx
Normal file
25
src/ui/component/rewardTotal/view.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import TotalBackground from './total-background.png';
|
||||
import useTween from 'util/use-tween';
|
||||
|
||||
type Props = {
|
||||
rewards: Array<Reward>,
|
||||
};
|
||||
|
||||
function RewardTotal(props: Props) {
|
||||
const { rewards } = props;
|
||||
const rewardTotal = rewards.reduce((acc, val) => acc + val.reward_amount, 0);
|
||||
const total = useTween(rewardTotal * 25);
|
||||
const integer = Math.round(total * rewardTotal);
|
||||
|
||||
return (
|
||||
<section className="card card--section card--reward-total" style={{ backgroundImage: `url(${TotalBackground})` }}>
|
||||
<span className="card__title">
|
||||
{integer} LBC {__('Earned From Rewards')}
|
||||
</span>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default RewardTotal;
|
|
@ -17,8 +17,10 @@ import InvitePage from 'page/invite';
|
|||
import SubscriptionsPage from 'page/subscriptions';
|
||||
import SearchPage from 'page/search';
|
||||
import UserHistoryPage from 'page/userHistory';
|
||||
import SendCreditsPage from 'page/sendCredits';
|
||||
import WalletPage from 'page/wallet';
|
||||
import NavigationHistory from 'page/navigationHistory';
|
||||
import TagsPage from 'page/tags';
|
||||
import TagsEditPage from 'page/tagsEdit';
|
||||
|
||||
const Scroll = withRouter(function ScrollWrapper(props) {
|
||||
const { pathname } = props.location;
|
||||
|
@ -50,12 +52,12 @@ export default function AppRouter() {
|
|||
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
||||
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
|
||||
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
|
||||
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} />
|
||||
<Route path={`/$/${PAGES.LIBRARY}`} exact component={UserHistoryPage} />
|
||||
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
|
||||
<Route path={`/$/${PAGES.SEND}`} exact component={SendCreditsPage} />
|
||||
<Route path={`/$/${PAGES.HISTORY}`} exact component={UserHistoryPage} />
|
||||
<Route path={`/$/${PAGES.HISTORY}/all`} exact component={NavigationHistory} />
|
||||
|
||||
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
|
||||
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
|
||||
<Route path={`/$/${PAGES.TAGS}/edit`} exact component={TagsEditPage} />
|
||||
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||
{/* 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/:claimId" exact component={ShowPage} />
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions, doToast } from 'lbry-redux';
|
||||
import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions } from 'lbry-redux';
|
||||
import { doToggleSearchExpanded } from 'redux/actions/app';
|
||||
import { selectSearchOptionsExpanded } from 'redux/selectors/app';
|
||||
import analytics from 'analytics';
|
||||
import SearchOptions from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
@ -14,24 +13,6 @@ const select = state => ({
|
|||
const perform = dispatch => ({
|
||||
setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
|
||||
toggleSearchExpanded: () => dispatch(doToggleSearchExpanded()),
|
||||
onFeedbackPositive: query => {
|
||||
analytics.apiSearchFeedback(query, 1);
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __('Thanks for the feedback! You help make the app better for everyone.'),
|
||||
})
|
||||
);
|
||||
},
|
||||
onFeedbackNegative: query => {
|
||||
analytics.apiSearchFeedback(query, 0);
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __(
|
||||
'Thanks for the feedback. Mark has been notified and is currently walking over to his computer to work on this.'
|
||||
),
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -16,39 +16,20 @@ type Props = {
|
|||
options: {},
|
||||
expanded: boolean,
|
||||
toggleSearchExpanded: () => void,
|
||||
query: string,
|
||||
onFeedbackPositive: string => void,
|
||||
onFeedbackNegative: string => void,
|
||||
};
|
||||
|
||||
const SearchOptions = (props: Props) => {
|
||||
const {
|
||||
options,
|
||||
setSearchOption,
|
||||
expanded,
|
||||
toggleSearchExpanded,
|
||||
query,
|
||||
onFeedbackPositive,
|
||||
onFeedbackNegative,
|
||||
} = props;
|
||||
const { options, setSearchOption, expanded, toggleSearchExpanded } = props;
|
||||
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
|
||||
|
||||
return (
|
||||
<div className="search__options-wrapper">
|
||||
<div className="card--space-between">
|
||||
<Button
|
||||
button="alt"
|
||||
label={__('FILTER')}
|
||||
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
|
||||
onClick={toggleSearchExpanded}
|
||||
/>
|
||||
|
||||
<div className="media__action-group">
|
||||
<span>{__('Find what you were looking for?')}</span>
|
||||
<Button button="alt" description={__('Yes')} onClick={() => onFeedbackPositive(query)} icon={ICONS.YES} />
|
||||
<Button button="alt" description={__('No')} onClick={() => onFeedbackNegative(query)} icon={ICONS.NO} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
button="alt"
|
||||
label={__('FILTER')}
|
||||
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
|
||||
onClick={toggleSearchExpanded}
|
||||
/>
|
||||
<ExpandableOptions pose={expanded ? 'show' : 'hide'}>
|
||||
{expanded && (
|
||||
<Form className="card__content search__options">
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectUnreadAmount } from 'redux/selectors/subscriptions';
|
||||
import { selectShouldShowInviteGuide } from 'redux/selectors/app';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { selectFollowedTags, SETTINGS } from 'lbry-redux';
|
||||
import SideBar from './view';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
|
||||
const select = state => ({
|
||||
unreadSubscriptionTotal: selectUnreadAmount(state),
|
||||
shouldShowInviteGuide: selectShouldShowInviteGuide(state),
|
||||
subscriptions: selectSubscriptions(state),
|
||||
followedTags: selectFollowedTags(state),
|
||||
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
||||
});
|
||||
|
||||
const perform = () => ({});
|
||||
|
|
|
@ -3,107 +3,79 @@ import * as PAGES from 'constants/pages';
|
|||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import Tag from 'component/tag';
|
||||
|
||||
type Props = {
|
||||
unreadSubscriptionTotal: number,
|
||||
shouldShowInviteGuide: string,
|
||||
subscriptions: Array<Subscription>,
|
||||
followedTags: Array<Tag>,
|
||||
};
|
||||
|
||||
class SideBar extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { unreadSubscriptionTotal, shouldShowInviteGuide } = this.props;
|
||||
const buildLink = (path, label, icon, guide) => ({
|
||||
navigate: path ? `$/${path}` : '/',
|
||||
label,
|
||||
icon,
|
||||
guide,
|
||||
});
|
||||
function SideBar(props: Props) {
|
||||
const { subscriptions, followedTags } = props;
|
||||
const buildLink = (path, label, icon, guide) => ({
|
||||
navigate: path ? `$/${path}` : '/',
|
||||
label,
|
||||
icon,
|
||||
guide,
|
||||
});
|
||||
|
||||
const renderLink = (linkProps, index) => {
|
||||
const { guide } = linkProps;
|
||||
const renderLink = linkProps => (
|
||||
<li key={linkProps.label}>
|
||||
<Button {...linkProps} className="navigation__link" activeClass="navigation__link--active" />
|
||||
</li>
|
||||
);
|
||||
|
||||
const inner = (
|
||||
<Button
|
||||
{...linkProps}
|
||||
className={classnames('navigation__link', {
|
||||
'navigation__link--guide': guide,
|
||||
})}
|
||||
activeClass="navigation__link--active"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
{guide ? (
|
||||
<Tooltip key={guide} alwaysVisible direction="right" body={guide}>
|
||||
{inner}
|
||||
</Tooltip>
|
||||
) : (
|
||||
inner
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className="navigation-wrapper">
|
||||
<nav className="navigation">
|
||||
<ul className="navigation__links">
|
||||
{[
|
||||
{
|
||||
...buildLink(null, __('Discover'), ICONS.DISCOVER),
|
||||
...buildLink(null, __('Home'), ICONS.HOME),
|
||||
},
|
||||
{
|
||||
...buildLink(
|
||||
PAGES.SUBSCRIPTIONS,
|
||||
`${__('Subscriptions')} ${unreadSubscriptionTotal > 0 ? '(' + unreadSubscriptionTotal + ')' : ''}`,
|
||||
ICONS.SUBSCRIPTION
|
||||
),
|
||||
...buildLink(PAGES.SUBSCRIPTIONS, __('Subscriptions'), ICONS.SUBSCRIPTION),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.HISTORY, __('Library'), ICONS.DOWNLOAD),
|
||||
...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)}
|
||||
<li>
|
||||
<Button
|
||||
navigate="/$/tags/edit"
|
||||
icon={ICONS.EDIT}
|
||||
className="navigation__link"
|
||||
activeClass="navigation__link--active"
|
||||
label={__('Following')}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul className="navigation__links navigation__links--bottom">
|
||||
{[
|
||||
{
|
||||
...buildLink(PAGES.HELP, __('Help'), ICONS.HELP),
|
||||
},
|
||||
].map(renderLink)}
|
||||
<ul className="navigation__links tags--vertical">
|
||||
{followedTags.map(({ name }, key) => (
|
||||
<li className="navigation__link--indented" key={name}>
|
||||
<Tag navigate={`/$/tags?t${name}`} name={name} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="navigation__links--small">
|
||||
{subscriptions.map(({ uri, channelName }, index) => (
|
||||
<li key={uri} className="navigation__link--indented">
|
||||
<Button
|
||||
navigate={uri}
|
||||
label={channelName}
|
||||
className="navigation__link"
|
||||
activeClass="navigation__link--active"
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideBar;
|
||||
|
|
|
@ -66,8 +66,8 @@ class Spinner extends PureComponent<Props, State> {
|
|||
className={classnames('spinner', {
|
||||
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
|
||||
'spinner--light': !dark && (light || theme === DARK_THEME),
|
||||
'spinner--splash': type === 'splash',
|
||||
'spinner--small': type === 'small',
|
||||
'spinner--splash': type === 'splash',
|
||||
})}
|
||||
>
|
||||
<div className="rect rect1" />
|
||||
|
|
|
@ -19,10 +19,9 @@ type Props = {
|
|||
doOpenModal: (id: string) => void,
|
||||
showSnackBarOnSubscribe: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
buttonStyle: string,
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
export default function SubscribeButton(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
doChannelSubscribe,
|
||||
|
@ -32,19 +31,18 @@ export default (props: Props) => {
|
|||
isSubscribed,
|
||||
showSnackBarOnSubscribe,
|
||||
doToast,
|
||||
buttonStyle,
|
||||
} = props;
|
||||
|
||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
||||
const subscriptionLabel = isSubscribed ? __('Subscribed') : __('Subscribe');
|
||||
|
||||
const { claimName } = parseURI(uri);
|
||||
|
||||
return (
|
||||
<Button
|
||||
iconColor="red"
|
||||
icon={isSubscribed ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIPTION}
|
||||
button={buttonStyle || 'alt'}
|
||||
icon={ICONS.SUBSCRIPTION}
|
||||
button={'alt'}
|
||||
label={subscriptionLabel}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
@ -64,4 +62,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);
|
32
src/ui/component/tag/view.jsx
Normal file
32
src/ui/component/tag/view.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
type?: string,
|
||||
onClick?: any => any,
|
||||
disabled: boolean,
|
||||
};
|
||||
|
||||
export default function Tag(props: Props) {
|
||||
const { name, onClick, type = 'link', disabled = false } = props;
|
||||
|
||||
const clickProps = onClick ? { onClick } : { navigate: `/$/tags?t=${name}` };
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...clickProps}
|
||||
disabled={disabled}
|
||||
className={classnames('tag', {
|
||||
'tag--add': type === 'add',
|
||||
'tag--remove': type === 'remove',
|
||||
})}
|
||||
label={name}
|
||||
iconSize={12}
|
||||
iconRight={type !== 'link' && (type === 'remove' ? ICONS.CLOSE : ICONS.ADD)}
|
||||
/>
|
||||
);
|
||||
}
|
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);
|
64
src/ui/component/tagsSelect/view.jsx
Normal file
64
src/ui/component/tagsSelect/view.jsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// @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>
|
||||
<p className="help">{__("The tags you follow will change what's trending for you.")}</p>
|
||||
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -6,6 +6,7 @@ import {
|
|||
selectSupportsByOutpoint,
|
||||
selectTransactionListFilter,
|
||||
doSetTransactionListFilter,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import TransactionList from './view';
|
||||
|
||||
|
@ -14,6 +15,7 @@ const select = state => ({
|
|||
mySupports: selectSupportsByOutpoint(state),
|
||||
myClaims: selectAllMyClaimsByOutpoint(state),
|
||||
filterSetting: selectTransactionListFilter(state),
|
||||
loading: selectIsFetchingTransactions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -73,9 +73,7 @@ class TransactionListItem extends React.PureComponent<Props> {
|
|||
<td className="table__item--actionable">
|
||||
{reward && <span>{reward.reward_title}</span>}
|
||||
{claimName && claimId && (
|
||||
<Button button="link" navigate={buildURI({ claimName: claimName, claimId })}>
|
||||
{claimName}
|
||||
</Button>
|
||||
<Button button="link" navigate={buildURI({ claimName: claimName, claimId })} label={claimName} />
|
||||
)}
|
||||
</td>
|
||||
|
||||
|
|
|
@ -7,17 +7,21 @@ import Button from 'component/button';
|
|||
import FileExporter from 'component/common/file-exporter';
|
||||
import { TRANSACTIONS } from 'lbry-redux';
|
||||
import TransactionListItem from './internal/transaction-list-item';
|
||||
import RefreshTransactionButton from 'component/transactionRefreshButton';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
emptyMessage: ?string,
|
||||
slim?: boolean,
|
||||
transactions: Array<Transaction>,
|
||||
rewards: {},
|
||||
openModal: (id: string, { nout: number, txid: string }) => void,
|
||||
filterSetting: string,
|
||||
loading: boolean,
|
||||
mySupports: {},
|
||||
myClaims: any,
|
||||
filterSetting: string,
|
||||
openModal: (id: string, { nout: number, txid: string }) => void,
|
||||
rewards: {},
|
||||
setTransactionFilter: string => void,
|
||||
slim?: boolean,
|
||||
title: string,
|
||||
transactions: Array<Transaction>,
|
||||
};
|
||||
|
||||
class TransactionList extends React.PureComponent<Props> {
|
||||
|
@ -53,8 +57,7 @@ class TransactionList extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { emptyMessage, rewards, transactions, slim, filterSetting } = this.props;
|
||||
|
||||
const { emptyMessage, rewards, transactions, slim, filterSetting, title, loading } = this.props;
|
||||
// The shorter "recent transactions" list shouldn't be filtered
|
||||
const transactionList = slim ? transactions : transactions.filter(this.filterTransaction);
|
||||
|
||||
|
@ -65,9 +68,23 @@ class TransactionList extends React.PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<header className="card__header">
|
||||
{!slim && !!transactions.length && (
|
||||
<div className="card__actions card__actions--between card__actions--top-space">
|
||||
<header className="table__header">
|
||||
<h2 className="card__title card__title--flex-between">
|
||||
<span>
|
||||
{title}
|
||||
{loading && <Spinner type="small" />}
|
||||
</span>
|
||||
<div className="card__actions">
|
||||
{slim && (
|
||||
<Button button="link" className="button--alt" navigate="/$/transactions" label={__('Full History')} />
|
||||
)}
|
||||
<RefreshTransactionButton />
|
||||
</div>
|
||||
</h2>
|
||||
</header>
|
||||
{!slim && !!transactions.length && (
|
||||
<header className="card__header table__header">
|
||||
<div className="card__actions card__actions--between">
|
||||
<FileExporter
|
||||
data={transactionList}
|
||||
label={__('Export')}
|
||||
|
@ -100,9 +117,12 @@ class TransactionList extends React.PureComponent<Props> {
|
|||
</FormField>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
{!transactionList.length && <p className="card__subtitle">{emptyMessage || __('No transactions to list.')}</p>}
|
||||
</header>
|
||||
)}
|
||||
|
||||
{!loading && !transactionList.length && (
|
||||
<p className="main--empty empty">{emptyMessage || __('No transactions.')}</p>
|
||||
)}
|
||||
|
||||
{!!transactionList.length && (
|
||||
<React.Fragment>
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchTransactions,
|
||||
selectRecentTransactions,
|
||||
selectHasTransactions,
|
||||
selectIsFetchingTransactions,
|
||||
doFetchClaimListMine,
|
||||
} from 'lbry-redux';
|
||||
import { doFetchTransactions, selectRecentTransactions, doFetchClaimListMine } from 'lbry-redux';
|
||||
import TransactionListRecent from './view';
|
||||
|
||||
const select = state => ({
|
||||
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||
transactions: selectRecentTransactions(state),
|
||||
hasTransactions: selectHasTransactions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
// @flow
|
||||
import * as icons from 'constants/icons';
|
||||
import React, { Fragment } from 'react';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import Button from 'component/button';
|
||||
import React from 'react';
|
||||
import TransactionList from 'component/transactionList';
|
||||
import RefreshTransactionButton from 'component/transactionRefreshButton';
|
||||
|
||||
type Props = {
|
||||
fetchTransactions: () => void,
|
||||
|
@ -23,42 +19,15 @@ class TransactionListRecent extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fetchingTransactions, hasTransactions, transactions } = this.props;
|
||||
const { transactions } = this.props;
|
||||
return (
|
||||
<section className="card card--section">
|
||||
<header className="card__header card__header--flat">
|
||||
<h2 className="card__title card__title--flex-between">
|
||||
{__('Recent Transactions')}
|
||||
<RefreshTransactionButton />
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
{fetchingTransactions && !hasTransactions && (
|
||||
<div className="card__content">
|
||||
<BusyIndicator message={__('Loading transactions')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fetchingTransactions && !hasTransactions && (
|
||||
<div className="card__content">
|
||||
<p className="card__subtitle">{__('No transactions... yet.')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasTransactions && (
|
||||
<Fragment>
|
||||
<div className="card__content">
|
||||
<TransactionList
|
||||
slim
|
||||
transactions={transactions}
|
||||
emptyMessage={__("Looks like you don't have any recent transactions.")}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<Button button="primary" navigate="/$/transactions" label={__('Full History')} icon={icons.HISTORY} />
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
<section className="card card__content">
|
||||
<TransactionList
|
||||
slim
|
||||
title={__('Recent Transactions')}
|
||||
transactions={transactions}
|
||||
emptyMessage={__("Looks like you don't have any recent transactions.")}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,7 @@ type Props = {
|
|||
isResolvingUri: boolean,
|
||||
channelUri: ?string,
|
||||
link: ?boolean,
|
||||
claim: ?StreamClaim,
|
||||
channelClaim: ?ChannelClaim,
|
||||
claim: ?Claim,
|
||||
// Lint thinks we aren't using these, even though we are.
|
||||
// Possibly because the resolve function is an arrow function that is passed in props?
|
||||
resolveUri: string => void,
|
||||
|
|
18
src/ui/component/userEmail/index.js
Normal file
18
src/ui/component/userEmail/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectEmailToVerify, doUserResendVerificationEmail, doUserCheckEmailVerified, selectUser } from 'lbryinc';
|
||||
import UserEmailVerify from './view';
|
||||
|
||||
const select = state => ({
|
||||
email: selectEmailToVerify(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)),
|
||||
checkEmailVerified: () => dispatch(doUserCheckEmailVerified()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(UserEmailVerify);
|
60
src/ui/component/userEmail/view.jsx
Normal file
60
src/ui/component/userEmail/view.jsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form';
|
||||
import UserEmailNew from 'component/userEmailNew';
|
||||
import UserEmailVerify from 'component/userEmailVerify';
|
||||
|
||||
type Props = {
|
||||
cancelButton: React.Node,
|
||||
email: string,
|
||||
resendVerificationEmail: string => void,
|
||||
checkEmailVerified: () => void,
|
||||
user: {
|
||||
has_verified_email: boolean,
|
||||
},
|
||||
};
|
||||
|
||||
function UserEmail(props: Props) {
|
||||
const { email, user } = props;
|
||||
let isVerified = false;
|
||||
if (user) {
|
||||
isVerified = user.has_verified_email;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card card--section">
|
||||
{!email && <UserEmailNew />}
|
||||
{user && email && !isVerified && <UserEmailVerify />}
|
||||
{email && isVerified && (
|
||||
<React.Fragment>
|
||||
<div className="card__header">
|
||||
<h2 className="card__title">{__('Email')}</h2>
|
||||
<p className="card__subtitle">
|
||||
{email && isVerified && __('Your email has been successfully verified')}
|
||||
{!email && __('')}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isVerified && (
|
||||
<FormField
|
||||
type="text"
|
||||
className="form-field--copyable"
|
||||
readOnly
|
||||
label={__('Your Email')}
|
||||
value={email}
|
||||
inputButton={<Button button="inverse" label={__('Change')} />}
|
||||
/>
|
||||
)}
|
||||
<p className="help">
|
||||
{`${__(
|
||||
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards.'
|
||||
)} `}
|
||||
</p>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserEmail;
|
|
@ -56,7 +56,7 @@ class UserEmailVerify extends React.PureComponent<Props> {
|
|||
</header>
|
||||
|
||||
<div className="card__content">
|
||||
<p>
|
||||
<p className="card__subtitle">
|
||||
{__('An email was sent to')} {email}.{' '}
|
||||
{__('Follow the link and you will be good to go. This will update automatically.')}
|
||||
</p>
|
||||
|
|
|
@ -102,7 +102,7 @@ class UserVerify extends React.PureComponent<Props> {
|
|||
<h2 className="card__title">{__('3) Proof via Chat')}</h2>
|
||||
<p className="card__subtitle">
|
||||
{__(
|
||||
'A moderator capable of approving you is typically available in the #verification channel of our chat room.'
|
||||
'A moderator capable of approving you is typically available in the #rewardsapproval channel of our chat room.'
|
||||
)}{' '}
|
||||
{__(
|
||||
'This process will likely involve providing proof of a stable and established online or real-life identity.'
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @flow
|
||||
import * as icons from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import CopyableText from 'component/copyableText';
|
||||
|
@ -27,7 +26,7 @@ class WalletAddress extends React.PureComponent<Props, State> {
|
|||
(this: any).toggleQR = this.toggleQR.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { checkAddressIsMine, receiveAddress, getNewAddress } = this.props;
|
||||
if (!receiveAddress) {
|
||||
getNewAddress();
|
||||
|
@ -62,9 +61,8 @@ class WalletAddress extends React.PureComponent<Props, State> {
|
|||
<div className="card__content">
|
||||
<div className="card__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
button="inverse"
|
||||
label={__('Get New Address')}
|
||||
icon={icons.REFRESH}
|
||||
onClick={getNewAddress}
|
||||
disabled={gettingNewAddress}
|
||||
/>
|
||||
|
|
|
@ -19,7 +19,9 @@ const WalletBalance = (props: Props) => {
|
|||
</header>
|
||||
<div className="card__content">
|
||||
<h3>{__('You currently have')}</h3>
|
||||
{(balance || balance === 0) && <CreditAmount large badge={false} amount={balance} precision={8} />}
|
||||
<span className="card__content--large">
|
||||
{(balance || balance === 0) && <CreditAmount badge={false} amount={balance} precision={8} />}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import { Formik } from 'formik';
|
||||
|
@ -82,7 +82,7 @@ class WalletSend extends React.PureComponent<Props> {
|
|||
</div>
|
||||
<div className="card__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
button="inverse"
|
||||
type="submit"
|
||||
label={__('Send')}
|
||||
disabled={
|
||||
|
|
|
@ -18,7 +18,6 @@ export const SHOW_MODAL = 'SHOW_MODAL';
|
|||
export const HIDE_MODAL = 'HIDE_MODAL';
|
||||
export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED';
|
||||
export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED';
|
||||
export const ENNNHHHAAANNNCEEE = 'ENNNHHHAAANNNCEEE';
|
||||
|
||||
// Navigation
|
||||
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';
|
||||
|
@ -193,8 +192,6 @@ export const SET_VIEW_MODE = 'SET_VIEW_MODE';
|
|||
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
|
||||
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
|
||||
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
|
||||
export const SUBSCRIPTION_FIRST_RUN_COMPLETED = 'SUBSCRIPTION_FIRST_RUN_COMPLETED';
|
||||
export const VIEW_SUGGESTED_SUBSCRIPTIONS = 'VIEW_SUGGESTED_SUBSCRIPTIONS';
|
||||
|
||||
// Publishing
|
||||
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
||||
|
|
|
@ -13,6 +13,7 @@ export const DOWNLOAD = 'Download';
|
|||
export const UPLOAD = 'UploadCloud';
|
||||
export const PUBLISHED = 'Cloud';
|
||||
export const CLOSE = 'X';
|
||||
export const ADD = 'Plus';
|
||||
export const EDIT = 'Edit3';
|
||||
export const DELETE = 'Trash';
|
||||
export const REPORT = 'Flag';
|
||||
|
@ -23,6 +24,8 @@ export const CHANNEL = 'AtSign';
|
|||
export const REFRESH = 'RefreshCw';
|
||||
export const HISTORY = 'Clock';
|
||||
export const HOME = 'Home';
|
||||
export const OVERVIEW = 'Activity';
|
||||
export const WALLET = 'List';
|
||||
export const PHONE = 'Phone';
|
||||
export const COMPLETE = 'Check';
|
||||
export const COMPLETED = 'CheckCircle';
|
||||
|
@ -41,6 +44,7 @@ export const ACCOUNT = 'User';
|
|||
export const SETTINGS = 'Settings';
|
||||
export const INVITE = 'Users';
|
||||
export const FILE = 'File';
|
||||
export const FULLSCREEN = 'Maximize';
|
||||
export const OPTIONS = 'Sliders';
|
||||
export const YES = 'ThumbsUp';
|
||||
export const NO = 'ThumbsDown';
|
||||
|
@ -63,3 +67,6 @@ export const MUSIC_ALBUM = 'Disc';
|
|||
export const MUSIC_ARTIST = 'Mic';
|
||||
export const MUSIC_SONG = 'Music';
|
||||
export const MUSIC_EQUALIZER = 'Sliders';
|
||||
export const LIGHT = 'Sun';
|
||||
export const DARK = 'Moon';
|
||||
export const AUTO = 'Clock';
|
||||
|
|
|
@ -4,7 +4,7 @@ export const CHANNEL = 'channel';
|
|||
export const DISCOVER = 'discover';
|
||||
export const DOWNLOADED = 'downloaded';
|
||||
export const HELP = 'help';
|
||||
export const HISTORY = 'history';
|
||||
export const LIBRARY = 'library';
|
||||
export const INVITE = 'invite';
|
||||
export const PUBLISH = 'publish';
|
||||
export const PUBLISHED = 'published';
|
||||
|
@ -18,3 +18,5 @@ export const ACCOUNT = 'account';
|
|||
export const SUBSCRIPTIONS = 'subscriptions';
|
||||
export const SEARCH = 'search';
|
||||
export const TRANSACTIONS = 'transactions';
|
||||
export const TAGS = 'tags';
|
||||
export const WALLET = 'wallet';
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
|
||||
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
|
||||
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
|
||||
export const FIRST_RUN_COMPLETED = 'first_run_completed';
|
||||
export const INVITE_ACKNOWLEDGED = 'invite_acknowledged';
|
||||
export const LANGUAGE = 'language';
|
||||
export const SHOW_NSFW = 'showNsfw';
|
||||
|
|
14
src/ui/constants/tags.js
Normal file
14
src/ui/constants/tags.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const defaultFollowedTags = [
|
||||
'blockchain',
|
||||
'news',
|
||||
'learning',
|
||||
'technology',
|
||||
'automotive',
|
||||
'economics',
|
||||
'food',
|
||||
'science',
|
||||
'art',
|
||||
'nature',
|
||||
];
|
||||
|
||||
export const defaultKnownTags = ['beliefs', 'funny', 'gaming', 'pop culture', 'music', 'sports', 'weapons'];
|
|
@ -13,7 +13,7 @@ import ReactDOM from 'react-dom';
|
|||
import { Provider } from 'react-redux';
|
||||
import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app';
|
||||
import { Lbry, doToast, isURIValid, setSearchApi } from 'lbry-redux';
|
||||
import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings';
|
||||
import { doInitLanguage, doUpdateIsNightAsync } from 'redux/actions/settings';
|
||||
import { doAuthenticate, Lbryio, rewards, doBlackListedOutpointsSubscribe } from 'lbryinc';
|
||||
import { store, history } from 'store';
|
||||
import pjson from 'package.json';
|
||||
|
@ -29,7 +29,7 @@ import { formatLbryUriForWeb } from 'util/uri';
|
|||
import 'scss/all.scss';
|
||||
|
||||
const APPPAGEURL = 'lbry://?';
|
||||
|
||||
const COOKIE_EXPIRE_TIME = 60 * 60 * 24 * 365; // 1 year
|
||||
// @if TARGET='app'
|
||||
const { autoUpdater } = remote.require('electron-updater');
|
||||
autoUpdater.logger = remote.require('electron-log');
|
||||
|
@ -73,8 +73,11 @@ Lbryio.setOverride(
|
|||
|
||||
const newAuthToken = response.auth_token;
|
||||
authToken = newAuthToken;
|
||||
|
||||
// @if TARGET='web'
|
||||
document.cookie = cookie.serialize('auth_token', authToken);
|
||||
document.cookie = cookie.serialize('auth_token', authToken, {
|
||||
maxAge: COOKIE_EXPIRE_TIME,
|
||||
});
|
||||
// @endif
|
||||
// @if TARGET='app'
|
||||
ipcRenderer.send('set-auth-token', authToken);
|
||||
|
@ -157,6 +160,13 @@ ipcRenderer.on('window-is-focused', () => {
|
|||
ipcRenderer.on('devtools-is-opened', () => {
|
||||
doLogWarningConsoleMessage();
|
||||
});
|
||||
|
||||
// Force exit mode for html5 fullscreen api
|
||||
// See: https://github.com/electron/electron/issues/18188
|
||||
remote.getCurrentWindow().on('leave-full-screen', event => {
|
||||
document.webkitExitFullscreen();
|
||||
});
|
||||
|
||||
// @endif
|
||||
|
||||
document.addEventListener('dragover', event => {
|
||||
|
@ -204,7 +214,7 @@ const init = () => {
|
|||
app.store.dispatch(doUpdateIsNightAsync());
|
||||
// @endif
|
||||
|
||||
app.store.dispatch(doDownloadLanguages());
|
||||
app.store.dispatch(doInitLanguage());
|
||||
app.store.dispatch(doBlackListedOutpointsSubscribe());
|
||||
|
||||
function onDaemonReady() {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import * as React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import Button from 'component/button';
|
||||
import app from 'app';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type ModalProps = {
|
||||
|
@ -20,7 +19,6 @@ type ModalProps = {
|
|||
extraContent?: React.Node,
|
||||
expandButtonLabel?: string,
|
||||
hideButtonLabel?: string,
|
||||
fullScreen: boolean,
|
||||
title?: string | React.Node,
|
||||
};
|
||||
|
||||
|
@ -32,7 +30,6 @@ export class Modal extends React.PureComponent<ModalProps> {
|
|||
abortButtonLabel: __('Cancel'),
|
||||
confirmButtonDisabled: false,
|
||||
abortButtonDisabled: false,
|
||||
fullScreen: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -45,7 +42,6 @@ export class Modal extends React.PureComponent<ModalProps> {
|
|||
abortButtonLabel,
|
||||
abortButtonDisabled,
|
||||
onAborted,
|
||||
fullScreen,
|
||||
className,
|
||||
title,
|
||||
...modalProps
|
||||
|
@ -54,10 +50,7 @@ export class Modal extends React.PureComponent<ModalProps> {
|
|||
<ReactModal
|
||||
{...modalProps}
|
||||
onRequestClose={onAborted || onConfirmed}
|
||||
className={classnames('card', className, {
|
||||
modal: !fullScreen,
|
||||
'modal--fullscreen': fullScreen,
|
||||
})}
|
||||
className={classnames('card card--modal modal', className)}
|
||||
overlayClassName="modal-overlay"
|
||||
>
|
||||
{title && (
|
||||
|
|
|
@ -40,7 +40,7 @@ class ModalAffirmPurchase extends React.PureComponent<Props> {
|
|||
onAborted={cancelPurchase}
|
||||
>
|
||||
<section className="card__content">
|
||||
<p>
|
||||
<p className="card__subtitle">
|
||||
{__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '}
|
||||
<strong>
|
||||
<FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} />
|
||||
|
|
|
@ -50,8 +50,6 @@ function ModalAutoGenerateThumbnail(props: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log('resized');
|
||||
|
||||
const fixedWidth = 450;
|
||||
const videoWidth = player.videoWidth;
|
||||
const videoHeight = player.videoHeight;
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import WalletPage from './view';
|
||||
import { connect } from 'react-redux';
|
||||
import AccountPage from './view';
|
||||
|
||||
export default WalletPage;
|
||||
const select = state => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(AccountPage);
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import WalletBalance from 'component/walletBalance';
|
||||
import RewardSummary from 'component/rewardSummary';
|
||||
import TransactionListRecent from 'component/transactionListRecent';
|
||||
import WalletAddress from 'component/walletAddress';
|
||||
import RewardTotal from 'component/rewardTotal';
|
||||
import Page from 'component/page';
|
||||
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
|
||||
import WalletSend from 'component/walletSend';
|
||||
import UserEmail from 'component/userEmail';
|
||||
import InvitePage from 'page/invite';
|
||||
|
||||
const WalletPage = () => (
|
||||
<Page>
|
||||
{IS_WEB && <UnsupportedOnWeb />}
|
||||
<div className={classnames({ 'card--disabled': IS_WEB })}>
|
||||
<div className="columns">
|
||||
<WalletBalance />
|
||||
<RewardSummary />
|
||||
<UserEmail />
|
||||
<div>
|
||||
<RewardSummary />
|
||||
<RewardTotal />
|
||||
</div>
|
||||
</div>
|
||||
<TransactionListRecent />
|
||||
<InvitePage />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectAuthenticationIsPending,
|
||||
selectEmailToVerify,
|
||||
selectUserIsVerificationCandidate,
|
||||
selectUser,
|
||||
selectUserIsPending,
|
||||
selectIdentityVerifyIsPending,
|
||||
} from 'lbryinc';
|
||||
import { selectEmailToVerify, selectUser } from 'lbryinc';
|
||||
import AuthPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
isPending: selectAuthenticationIsPending(state) || selectUserIsPending(state) || selectIdentityVerifyIsPending(state),
|
||||
email: selectEmailToVerify(state),
|
||||
user: selectUser(state),
|
||||
isVerificationCandidate: selectUserIsVerificationCandidate(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import Button from 'component/button';
|
||||
import UserEmailNew from 'component/userEmailNew';
|
||||
import UserEmailVerify from 'component/userEmailVerify';
|
||||
import UserEmail from 'component/userEmail';
|
||||
import UserVerify from 'component/userVerify';
|
||||
import Page from 'component/page';
|
||||
|
||||
type Props = {
|
||||
isPending: boolean,
|
||||
email: string,
|
||||
pathAfterAuth: string,
|
||||
location: UrlLocation,
|
||||
history: { push: string => void },
|
||||
user: ?{
|
||||
|
@ -21,12 +17,12 @@ type Props = {
|
|||
};
|
||||
|
||||
class AuthPage extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.navigateIfAuthenticated(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.navigateIfAuthenticated(nextProps);
|
||||
componentDidUpdate() {
|
||||
this.navigateIfAuthenticated(this.props);
|
||||
}
|
||||
|
||||
navigateIfAuthenticated = (props: Props) => {
|
||||
|
@ -40,42 +36,9 @@ class AuthPage extends React.PureComponent<Props> {
|
|||
}
|
||||
};
|
||||
|
||||
renderMain() {
|
||||
const { email, isPending, user } = this.props;
|
||||
|
||||
if (isPending) {
|
||||
return [<BusyIndicator message={__('Authenticating')} />, true];
|
||||
} else if (user && !user.has_verified_email && !email) {
|
||||
return [<UserEmailNew />, true];
|
||||
} else if (user && !user.has_verified_email) {
|
||||
return [<UserEmailVerify />, true];
|
||||
} else if (user && !user.is_identity_verified) {
|
||||
return [<UserVerify />, false];
|
||||
}
|
||||
return [<span className="empty">{__('No further steps.')}</span>, true];
|
||||
}
|
||||
|
||||
render() {
|
||||
const [innerContent, useTemplate] = this.renderMain();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{useTemplate ? (
|
||||
<section className="card card--section">
|
||||
{innerContent}
|
||||
|
||||
<p className="help">
|
||||
{`${__(
|
||||
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.'
|
||||
)} `}
|
||||
<Button button="link" navigate="/" label={__('Return home.')} />
|
||||
</p>
|
||||
</section>
|
||||
) : (
|
||||
innerContent
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
const { user, email } = this.props;
|
||||
return <Page>{user && email && !user.is_identity_verified ? <UserVerify /> : <UserEmail />}</Page>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchRewardedContent,
|
||||
doRewardList,
|
||||
selectFeaturedUris,
|
||||
doFetchFeaturedUris,
|
||||
selectFetchingFeaturedUris,
|
||||
} from 'lbryinc';
|
||||
import { selectFollowedTags } from 'lbry-redux';
|
||||
import DiscoverPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
featuredUris: selectFeaturedUris(state),
|
||||
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
|
||||
followedTags: selectFollowedTags(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
});
|
||||
const perform = {};
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
|
|
|
@ -1,82 +1,24 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import TagsSelect from 'component/tagsSelect';
|
||||
import Page from 'component/page';
|
||||
import CategoryList from 'component/categoryList';
|
||||
import FirstRun from 'component/firstRun';
|
||||
|
||||
type Props = {
|
||||
fetchFeaturedUris: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
fetchRewards: () => void,
|
||||
fetchingFeaturedUris: boolean,
|
||||
featuredUris: {},
|
||||
followedTags: Array<Tag>,
|
||||
};
|
||||
|
||||
class DiscoverPage extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
this.continousFetch = undefined;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { fetchFeaturedUris, fetchRewardedContent, fetchRewards } = this.props;
|
||||
fetchFeaturedUris();
|
||||
fetchRewardedContent();
|
||||
|
||||
this.continousFetch = setInterval(() => {
|
||||
fetchFeaturedUris();
|
||||
fetchRewardedContent();
|
||||
fetchRewards();
|
||||
}, 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearContinuousFetch();
|
||||
}
|
||||
|
||||
getCategoryLinkPartByCategory(category: string) {
|
||||
const channelName = category.substr(category.indexOf('@'));
|
||||
if (!channelName.includes('#')) {
|
||||
return null;
|
||||
}
|
||||
return channelName;
|
||||
}
|
||||
|
||||
trimClaimIdFromCategory(category: string) {
|
||||
return category.split('#')[0];
|
||||
}
|
||||
|
||||
continousFetch: ?IntervalID;
|
||||
|
||||
clearContinuousFetch() {
|
||||
if (this.continousFetch) {
|
||||
clearInterval(this.continousFetch);
|
||||
this.continousFetch = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { featuredUris, fetchingFeaturedUris } = this.props;
|
||||
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
|
||||
const failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<Page notContained isLoading={!hasContent && fetchingFeaturedUris} className="main--no-padding">
|
||||
<FirstRun />
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(category => (
|
||||
<CategoryList
|
||||
lazyLoad
|
||||
key={category}
|
||||
category={this.trimClaimIdFromCategory(category)}
|
||||
uris={featuredUris[category]}
|
||||
categoryLink={this.getCategoryLinkPartByCategory(category)}
|
||||
/>
|
||||
))}
|
||||
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
function DiscoverPage(props: Props) {
|
||||
const { followedTags } = props;
|
||||
return (
|
||||
<Page>
|
||||
<ClaimListDiscover
|
||||
personal
|
||||
tags={followedTags.map(tag => tag.name)}
|
||||
injectedItem={<TagsSelect showClose title={__('Make This Your Own')} />}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiscoverPage;
|
||||
|
|
|
@ -49,7 +49,7 @@ const perform = dispatch => ({
|
|||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
prepareEdit: (publishData, uri) => dispatch(doPrepareEdit(publishData, uri)),
|
||||
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue