Add scam alert message when opening development tools. #1500

Merged
dan1d merged 3 commits from alert-scam into master 2018-06-01 04:10:14 +02:00
61 changed files with 623 additions and 448 deletions
Showing only changes of commit c38f34dbd0 - Show all commits

View file

@ -36,6 +36,7 @@
"func-names": ["warn", "as-needed"], "func-names": ["warn", "as-needed"],
"jsx-a11y/label-has-for": 0, "jsx-a11y/label-has-for": 0,
"import/prefer-default-export": 0, "import/prefer-default-export": 0,
"no-return-assign": 0 "no-return-assign": 0,
"react/require-default-props": 0
} }
} }

View file

@ -1,16 +1,19 @@
matrix: matrix:
include: include:
- os: osx - os: osx
env: TARGET=mac
osx_image: xcode9.2 osx_image: xcode9.2
language: node_js language: node_js
node_js: "9" node_js: "9"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- os: linux - os: linux
env: TARGET=windows
services: docker services: docker
language: node_js language: node_js
node_js: "9" node_js: "9"
- os: linux
env: TARGET=linux
language: node_js
node_js: "9"
cache: false cache: false
before_install: before_install:
- | - |
@ -20,6 +23,7 @@ before_install:
else else
sudo apt-get -qq update sudo apt-get -qq update
sudo apt-get install -y libsecret-1-dev sudo apt-get install -y libsecret-1-dev
sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib
curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.6.0 curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.6.0
export PATH="$HOME/.yarn/bin:$PATH" export PATH="$HOME/.yarn/bin:$PATH"
fi fi
@ -27,17 +31,15 @@ before_script:
- git lfs pull - git lfs pull
script: script:
- | - |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then if [ "$TARGET" == "windows" ]; then
docker run --rm \ docker run --rm \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \ --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
-v ${PWD}:/project \ -v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \ electronuserland/builder:wine \
/bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn build --linux --win --publish onTag" /bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn build --win --publish onTag";
else
yarn build --publish onTag
fi fi
- if [ "$TARGET" == "mac" ]; then yarn build --publish onTag; fi
- if [ "$TARGET" == "linux" ]; then yarn --link-duplicates --pure-lockfile && yarn build --linux --publish onTag; fi
branches: branches:
except: except:
- "/^v\\d+\\.\\d+\\.\\d+$/" - "/^v\\d+\\.\\d+\\.\\d+$/"

View file

@ -14,11 +14,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
* Pre-fill publish URL after clicking "Put something here" link ([#1303](https://github.com/lbryio/lbry-app/pull/1303)) * Pre-fill publish URL after clicking "Put something here" link ([#1303](https://github.com/lbryio/lbry-app/pull/1303))
* Add Danger JS to automate code reviews ([#1289](https://github.com/lbryio/lbry-app/pull/1289)) * Add Danger JS to automate code reviews ([#1289](https://github.com/lbryio/lbry-app/pull/1289))
* Autoplay downloaded and free media ([#584](https://github.com/lbryio/lbry-app/pull/1453)) * Autoplay downloaded and free media ([#584](https://github.com/lbryio/lbry-app/pull/1453))
* Add 'Go to page' input on channel pagination ([#1166](https://github.com/lbryio/lbry-app/pull/1166))
* Add "View on web" button on file/channel pages with spee.ch link ([#1222](https://github.com/lbryio/lbry-app/pull/1222))
### Changed ### Changed
* Add flair to snackbar ([#1313](https://github.com/lbryio/lbry-app/pull/1313)) * Add flair to snackbar ([#1313](https://github.com/lbryio/lbry-app/pull/1313))
* Made font in price badge larger ([#1420](https://github.com/lbryio/lbry-app/pull/1420)) * Made font in price badge larger ([#1420](https://github.com/lbryio/lbry-app/pull/1420))
* Store subscriptions in internal database ([#1424](https://github.com/lbryio/lbry-app/pull/1424)) * Store subscriptions in internal database ([#1424](https://github.com/lbryio/lbry-app/pull/1424))
* Move rewards logic to interal api ([#1509](https://github.com/lbryio/lbry-app/pull/1509))
### Fixed ### Fixed
* Fix content-type not shown correctly in file description ([#863](https://github.com/lbryio/lbry-app/pull/863)) * Fix content-type not shown correctly in file description ([#863](https://github.com/lbryio/lbry-app/pull/863))
@ -57,7 +60,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
* App category for Linux ([#877](https://github.com/lbryio/lbry-app/pull/877)) * App category for Linux ([#877](https://github.com/lbryio/lbry-app/pull/877))
* Add YouTube Sync reward ([#1147](https://github.com/lbryio/lbry-app/pull/1147)) * Add YouTube Sync reward ([#1147](https://github.com/lbryio/lbry-app/pull/1147))
* Retain previous screen sizing on startup ([#338](https://github.com/lbryio/lbry-app/issues/338)) * Retain previous screen sizing on startup ([#338](https://github.com/lbryio/lbry-app/issues/338))
* Add 'Go to page' input on channel pagination ([#1166](https://github.com/lbryio/lbry-app/pull/1166))
### Changed ### Changed
@ -372,7 +374,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
* Removed placeholder values from price selection form fields, which was causing confusion that these were real values (#426) * Removed placeholder values from price selection form fields, which was causing confusion that these were real values (#426)
* Fixed showing "other currency" help tip in publish form, which was caused due to not "setting" state for price * Fixed showing "other currency" help tip in publish form, which was caused due to not "setting" state for price
* Publish page now properly checks for all required fields are filled * Publish page now properly checks for all required fields are filled
* Fixed pagination styling for pages > 5 (#416)
* Fixed sizing on squat videos (#419) * Fixed sizing on squat videos (#419)
* Support claims no longer show up on Published page (#384) * Support claims no longer show up on Published page (#384)
* Fixed rendering of small prices (#461) * Fixed rendering of small prices (#461)
@ -410,7 +411,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Added ### Added
* Replaced horizontal scrollbars with scroll arrows * Replaced horizontal scrollbars with scroll arrows
* Featured weekly reward content shows with an orange star * Featured weekly reward content shows with an orange star
* Added pagination to channel pages
### Fixed ### Fixed

View file

@ -146,6 +146,8 @@ There are a few tools integrated to the project that will ease the process of de
manner and, therefore, not begin working on anything reserved (or updated) within the last 3 days. manner and, therefore, not begin working on anything reserved (or updated) within the last 3 days.
If someone has been officially assigned an issue via Github's assignment system, it is also not If someone has been officially assigned an issue via Github's assignment system, it is also not
available. Contributors are encouraged to ask if they have any questions about issue availability. available. Contributors are encouraged to ask if they have any questions about issue availability.
* The [changelog](https://github.com/lbryio/lbry-app/blob/master/CHANGELOG.md) should be updated to
include a referene to the fix/change/addition. See previous entries for format.
* Once the pull request is visible in the LBRY repo, a LBRY team member will review it and make sure * Once the pull request is visible in the LBRY repo, a LBRY team member will review it and make sure
it is up to our standards. At this point, the contributor may have to change his or her code based it is up to our standards. At this point, the contributor may have to change his or her code based
on our suggestions and comments. on our suggestions and comments.

View file

@ -50,7 +50,7 @@ distributable packages.
#### Resetting your Packages #### Resetting your Packages
If the app isn't building, or `yarn xxx` commands aren't working you may need to just reset your `node_modules`. To do so you can run: `rm -r node_modules && yarn` or `del node_modules && yarn` on Windows. If the app isn't building, or `yarn xxx` commands aren't working you may need to just reset your `node_modules`. To do so you can run: `rm -r node_modules && yarn` or `del /s /q node_modules && yarn` on Windows.
## Contributing ## Contributing

View file

@ -48,7 +48,7 @@
"formik": "^0.10.4", "formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2", "hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1", "keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#30c18725d8c6c141c30c57f0a324d0abb8963b99", "lbry-redux": "lbryio/lbry-redux#fb27fb66387043be1b7962cd9439845fa73d7d8e",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"mixpanel-browser": "^2.17.1", "mixpanel-browser": "^2.17.1",
"moment": "^2.22.0", "moment": "^2.22.0",

View file

@ -3,7 +3,6 @@ import isDev from 'electron-is-dev';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
import setupBarMenu from './menu/setupBarMenu'; import setupBarMenu from './menu/setupBarMenu';
import setupContextMenu from './menu/setupContextMenu';
export default appState => { export default appState => {
// Get primary display dimensions from Electron. // Get primary display dimensions from Electron.
@ -73,7 +72,6 @@ export default appState => {
} }
setupBarMenu(); setupBarMenu();
setupContextMenu(window);
window.on('close', event => { window.on('close', event => {
if (!appState.isQuitting && !appState.autoUpdateAccepted) { if (!appState.isQuitting && !appState.autoUpdateAccepted) {

View file

@ -179,11 +179,11 @@ ipcMain.on('version-info-requested', () => {
}, },
}; };
let result = ''; let result = '';
const onSuccess = res => {
const req = https.get(Object.assign(opts, url.parse(latestReleaseAPIURL)), res => {
res.on('data', data => { res.on('data', data => {
result += data; result += data;
}); });
res.on('end', () => { res.on('end', () => {
const tagName = JSON.parse(result).tag_name; const tagName = JSON.parse(result).tag_name;
const [, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/); const [, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/);
@ -202,14 +202,27 @@ ipcMain.on('version-info-requested', () => {
} }
} }
}); });
}); };
req.on('error', err => { const requestLatestRelease = (apiUrl, alreadyRedirected = false) => {
console.log('Failed to get current version from GitHub. Error:', err); const req = https.get(Object.assign(opts, url.parse(apiUrl)), res => {
if (rendererWindow) { if (res.statusCode === 301 || res.statusCode === 302) {
rendererWindow.webContents.send('version-info-received', null); requestLatestRelease(res.headers.location, true);
} } else {
}); onSuccess(res);
}
});
if (alreadyRedirected) return;
req.on('error', err => {
console.log('Failed to get current version from GitHub. Error:', err);
if (rendererWindow) {
rendererWindow.webContents.send('version-info-received', null);
}
});
};
requestLatestRelease(latestReleaseAPIURL);
}); });
ipcMain.on('get-auth-token', event => { ipcMain.on('get-auth-token', event => {
@ -245,8 +258,11 @@ const isSecondInstance = app.makeSingleInstance(argv => {
// path, so we just strip it off. // path, so we just strip it off.
// - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as // - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as
// an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well. // an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well.
// - ? also interpreted as an anchor, remove slash also.
if (process.platform === 'win32') { if (process.platform === 'win32') {
URI = URI.replace(/\/$/, '').replace('/#', '#'); URI = URI.replace(/\/$/, '')
.replace('/#', '#')
.replace('/?', '?');
} }
rendererWindow.webContents.send('open-uri-requested', URI); rendererWindow.webContents.send('open-uri-requested', URI);

View file

@ -1,27 +0,0 @@
// @flow
import { Menu, BrowserWindow } from 'electron';
import isDev from 'electron-is-dev';
export default (rendererWindow: BrowserWindow) => {
rendererWindow.webContents.on('context-menu', (e, params) => {
const { x, y } = params;
const template = [{ role: 'cut' }, { role: 'copy' }, { role: 'paste' }];
const developmentTemplateAddition = [
{ type: 'separator' },
{
label: 'Inspect element',
click: () => {
rendererWindow.inspectElement(x, y);
},
},
];
if (isDev) {
template.push(...developmentTemplateAddition);
}
Menu.buildFromTemplate(template).popup();
});
};

View file

@ -22,6 +22,7 @@ type Props = {
button: ?string, // primary, secondary, alt, link button: ?string, // primary, secondary, alt, link
noPadding: ?boolean, // to remove padding and allow circular buttons noPadding: ?boolean, // to remove padding and allow circular buttons
uppercase: ?boolean, uppercase: ?boolean,
iconColor: ?string,
}; };
class Button extends React.PureComponent<Props> { class Button extends React.PureComponent<Props> {
@ -48,6 +49,7 @@ class Button extends React.PureComponent<Props> {
type, type,
noPadding, noPadding,
uppercase, uppercase,
iconColor,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -82,10 +84,10 @@ class Button extends React.PureComponent<Props> {
const content = ( const content = (
<span className="btn__content"> <span className="btn__content">
{icon && <Icon icon={icon} />} {icon && <Icon icon={icon} iconColor={iconColor} />}
{label && <span className="btn__label">{label}</span>} {label && <span className="btn__label">{label}</span>}
{children && children} {children && children}
{iconRight && <Icon icon={iconRight} />} {iconRight && <Icon icon={iconRight} iconColor={iconColor} />}
</span> </span>
); );

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { doFetchClaimsByChannel } from 'redux/actions/content';
import {
makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims,
} from 'lbry-redux';
import CategoryList from './view';
const select = (state, props) => ({
channelClaims: makeSelectClaimsInChannelForCurrentPage(props.categoryLink)(state),
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
});
const perform = dispatch => ({
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
});
export default connect(select, perform)(CategoryList);

View file

@ -5,11 +5,15 @@ import ToolTip from 'component/common/tooltip';
import FileCard from 'component/fileCard'; import FileCard from 'component/fileCard';
import Button from 'component/button'; import Button from 'component/button';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import Claim from 'types/claim';
type Props = { type Props = {
category: string, category: string,
names: Array<string>, names: Array<string>,
categoryLink?: string, categoryLink: ?string,
fetching: boolean,
channelClaims: Array<Claim>,
fetchChannel: string => void,
}; };
type State = { type State = {
@ -18,6 +22,11 @@ type State = {
}; };
class CategoryList extends React.PureComponent<Props, State> { class CategoryList extends React.PureComponent<Props, State> {
static defaultProps = {
names: [],
categoryLink: '',
};
constructor() { constructor() {
super(); super();
@ -32,6 +41,11 @@ class CategoryList extends React.PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
const { fetching, categoryLink, fetchChannel } = this.props;
if (!fetching) {
fetchChannel(categoryLink);
}
const cardRow = this.rowItems; const cardRow = this.rowItems;
if (cardRow) { if (cardRow) {
const cards = cardRow.getElementsByTagName('section'); const cards = cardRow.getElementsByTagName('section');
@ -109,6 +123,9 @@ class CategoryList extends React.PureComponent<Props, State> {
// check if a card is fully visible horizontally // check if a card is fully visible horizontally
isCardVisible = (section: HTMLElement) => { isCardVisible = (section: HTMLElement) => {
if (!section) {
return false;
}
const rect = section.getBoundingClientRect(); const rect = section.getBoundingClientRect();
const isVisible = rect.left >= 0 && rect.right <= window.innerWidth; const isVisible = rect.left >= 0 && rect.right <= window.innerWidth;
return isVisible; return isVisible;
@ -189,7 +206,7 @@ class CategoryList extends React.PureComponent<Props, State> {
} }
render() { render() {
const { category, names, categoryLink } = this.props; const { category, categoryLink, names, channelClaims } = this.props;
const { canScrollNext, canScrollPrevious } = this.state; const { canScrollNext, canScrollPrevious } = this.state;
// The lint was throwing an error saying we should use <button> instead of <a> // The lint was throwing an error saying we should use <button> instead of <a>
@ -235,6 +252,12 @@ class CategoryList extends React.PureComponent<Props, State> {
className="card-row__scrollhouse" className="card-row__scrollhouse"
> >
{names && names.map(name => <FileCard key={name} uri={normalizeURI(name)} />)} {names && names.map(name => <FileCard key={name} uri={normalizeURI(name)} />)}
{channelClaims && channelClaims.length
? channelClaims.map(claim => (
<FileCard key={claim.claim_id} uri={`lbry://${claim.name}#${claim.claim_id}`} />
))
: null}
</div> </div>
</div> </div>
); );

View file

@ -62,7 +62,6 @@ export class FormFieldPrice extends React.PureComponent<Props> {
name={`${name}_currency`} name={`${name}_currency`}
type="select" type="select"
id={`${name}_currency`} id={`${name}_currency`}
className="form-field"
disabled={disabled} disabled={disabled}
onChange={this.handleCurrencyChange} onChange={this.handleCurrencyChange}
value={price.currency} value={price.currency}

View file

@ -42,7 +42,7 @@ export class FormField extends React.PureComponent<Props> {
if (type) { if (type) {
if (type === 'select') { if (type === 'select') {
input = ( input = (
<select id={name} {...inputProps}> <select className="form-field__select" id={name} {...inputProps}>
{children} {children}
</select> </select>
); );

View file

@ -4,7 +4,6 @@ import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
type Props = { type Props = {
centered?: boolean,
children: React.Node, children: React.Node,
padded?: boolean, padded?: boolean,
verticallyCentered?: boolean, verticallyCentered?: boolean,
@ -22,7 +21,6 @@ export class FormRow extends React.PureComponent<Props> {
return ( return (
<div <div
className={classnames('form-row', { className={classnames('form-row', {
'form-row--centered': centered,
'form-row--padded': padded, 'form-row--padded': padded,
'form-row--vertically-centered': verticallyCentered, 'form-row--vertically-centered': verticallyCentered,
'form-row--stretch': stretch, 'form-row--stretch': stretch,

View file

@ -1,26 +1,52 @@
// @flow // @flow
import React from 'react'; import React from 'react';
// import * as icons from 'constants/icons';
import * as FeatherIcons from 'react-feather'; import * as FeatherIcons from 'react-feather';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import Tooltip from 'component/common/tooltip';
const RED_COLOR = '#e2495e'; const RED_COLOR = '#e2495e';
const PURPLE_COLOR = '#8165b0';
type Props = { type Props = {
icon: string, icon: string,
tooltip?: string, // tooltip direction
iconColor?: string,
}; };
class IconComponent extends React.PureComponent<Props> { class IconComponent extends React.PureComponent<Props> {
// TODO: Move all icons to constants and add titles for all getTooltip = (icon: string) => {
// Add some some sort of hover flyout with the title? switch (icon) {
case icons.FEATURED:
return __('Featured content. Earn rewards for watching.');
case icons.LOCAL:
return __('This file is downloaded.');
default:
return null;
}
};
getIconColor = (color: string) => {
switch (color) {
case 'red':
return RED_COLOR;
case 'purple':
return PURPLE_COLOR;
default:
return null;
}
};
render() { render() {
const { icon } = this.props; const { icon, tooltip, iconColor } = this.props;
const Icon = FeatherIcons[icon]; const Icon = FeatherIcons[icon];
if (!Icon) {
return null;
}
let color; let color;
if (icon === icons.HEART || icon === icons.FEATURED) { if (iconColor) {
color = RED_COLOR; color = this.getIconColor(iconColor);
} }
let size = 14; let size = 14;
@ -28,7 +54,19 @@ class IconComponent extends React.PureComponent<Props> {
size = 20; size = 20;
} }
return Icon ? <Icon size={size} className="icon" color={color} /> : null; let tooltipText;
if (tooltip) {
tooltipText = this.getTooltip(icon);
}
const inner = <Icon size={size} className="icon" color={color} />;
return tooltip ? (
<Tooltip icon body={tooltipText} direction={tooltip}>
{inner}
</Tooltip>
) : (
inner
);
} }
} }

View file

@ -1,55 +1,38 @@
// @flow // @flow
import React from 'react'; import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Icon from 'component/common/icon';
import Button from 'component/button';
import * as icons from 'constants/icons';
type Props = { type Props = {
body: string, body: string,
label: string, label?: string,
children: ?React.Node,
icon: ?boolean,
direction: string,
}; };
type State = { class ToolTip extends React.PureComponent<Props> {
showTooltip: boolean, static defaultProps = {
}; direction: 'bottom',
};
class ToolTip extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showTooltip: false,
};
(this: any).handleClick = this.handleClick.bind(this);
}
handleClick() {
const { showTooltip } = this.state;
if (!showTooltip) {
document.addEventListener('click', this.handleClick);
} else {
document.removeEventListener('click', this.handleClick);
}
this.setState({
showTooltip: !showTooltip,
});
}
render() { render() {
const { label, body } = this.props; const { children, label, body, icon, direction } = this.props;
const { showTooltip } = this.state;
const tooltipContent = children || label;
return ( return (
<span className="tooltip"> <span
<Button button="link" className="help tooltip__link" onClick={this.handleClick}> className={classnames('tooltip', {
{label} 'tooltip--label': label && !icon,
{showTooltip && <Icon icon={icons.CLOSE} />} 'tooltip--icon': icon,
</Button> 'tooltip--top': direction === 'top',
<div className={classnames('tooltip__body', { hidden: !showTooltip })}>{body}</div> 'tooltip--right': direction === 'right',
'tooltip--bottom': direction === 'bottom',
'tooltip--left': direction === 'left',
})}
>
{tooltipContent}
<span className="tooltip__body">{body}</span>
</span> </span>
); );
} }

View file

@ -1,7 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import FileDownloadLink from 'component/fileDownloadLink';
import { MODALS } from 'lbry-redux'; import { MODALS } from 'lbry-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
@ -27,21 +26,21 @@ class FileActions extends React.PureComponent<Props> {
return ( return (
<section className={classnames('card__actions', { 'card__actions--vertical': vertical })}> <section className={classnames('card__actions', { 'card__actions--vertical': vertical })}>
<FileDownloadLink uri={uri} />
{showDelete && ( {showDelete && (
<Button <Button
className="btn--file-actions" button="alt"
icon={icons.TRASH} icon={icons.TRASH}
description={__('Delete')} iconColor="red"
label={__('Delete')}
onClick={() => openModal({ id: MODALS.CONFIRM_FILE_REMOVE }, { uri })} onClick={() => openModal({ id: MODALS.CONFIRM_FILE_REMOVE }, { uri })}
/> />
)} )}
{!claimIsMine && ( {!claimIsMine && (
<Button <Button
className="btn--file-actions" button="alt"
icon={icons.REPORT} icon={icons.REPORT}
href={`https://lbry.io/dmca?claim_id=${claimId}`} href={`https://lbry.io/dmca?claim_id=${claimId}`}
description={__('Report content')} label={__('Report content')}
/> />
)} )}
</section> </section>

View file

@ -1,6 +1,6 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { normalizeURI } from 'lbry-redux'; import { normalizeURI, convertToShareLink } from 'lbry-redux';
import Button from 'component/button'; import Button from 'component/button';
import CardMedia from 'component/cardMedia'; import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text'; import TruncatedText from 'component/common/truncated-text';
@ -9,6 +9,7 @@ import FilePrice from 'component/filePrice';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import classnames from 'classnames'; import classnames from 'classnames';
import { openCopyLinkMenu } from '../../util/contextMenu';
// TODO: iron these out // TODO: iron these out
type Props = { type Props = {
@ -60,6 +61,9 @@ class FileCard extends React.PureComponent<Props> {
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw; const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const handleContextMenu = event => {
openCopyLinkMenu(convertToShareLink(uri), event);
};
// We should be able to tab through cards // We should be able to tab through cards
/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/click-events-have-key-events */
@ -72,6 +76,7 @@ class FileCard extends React.PureComponent<Props> {
'card--link': !pending, 'card--link': !pending,
'card--pending': pending, 'card--pending': pending,
})} })}
onContextMenu={handleContextMenu}
> >
<CardMedia nsfw={shouldObscureNsfw} thumbnail={thumbnail} /> <CardMedia nsfw={shouldObscureNsfw} thumbnail={thumbnail} />
<div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div> <div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div>
@ -98,14 +103,16 @@ class FileCard extends React.PureComponent<Props> {
<div className="card__title--small"> <div className="card__title--small">
<TruncatedText lines={3}>{title}</TruncatedText> <TruncatedText lines={3}>{title}</TruncatedText>
</div> </div>
<div className="card__subtitle card__subtitle--file-info"> <div className="card__subtitle">
{pending ? ( {pending ? (
<div>Pending...</div> <div>Pending...</div>
) : ( ) : (
<React.Fragment> <React.Fragment>
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
{isRewardContent && <Icon icon={icons.FEATURED} />} <div>
{fileInfo && <Icon icon={icons.LOCAL} />} {isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
</div>
</React.Fragment> </React.Fragment>
)} )}
</div> </div>

View file

@ -5,7 +5,6 @@ import {
makeSelectLoadingForUri, makeSelectLoadingForUri,
makeSelectCostInfoForUri, makeSelectCostInfoForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { doFetchAvailability } from 'redux/actions/availability';
import { doOpenFileInShell } from 'redux/actions/file'; import { doOpenFileInShell } from 'redux/actions/file';
import { doPurchaseUri, doStartDownload } from 'redux/actions/content'; import { doPurchaseUri, doStartDownload } from 'redux/actions/content';
import { doPause } from 'redux/actions/media'; import { doPause } from 'redux/actions/media';
@ -20,7 +19,6 @@ const select = (state, props) => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
checkAvailability: uri => dispatch(doFetchAvailability(uri)),
openInShell: path => dispatch(doOpenFileInShell(path)), openInShell: path => dispatch(doOpenFileInShell(path)),
purchaseUri: uri => dispatch(doPurchaseUri(uri)), purchaseUri: uri => dispatch(doPurchaseUri(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),

View file

@ -1,21 +1,29 @@
// @flow
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
class FileDownloadLink extends React.PureComponent { type Props = {
componentWillMount() { uri: string,
this.checkAvailability(this.props.uri); downloading: boolean,
} fileInfo: ?{
written_bytes: number,
componentWillReceiveProps(nextProps) { total_bytes: number,
this.checkAvailability(nextProps.uri); outpoint: number,
this.restartDownload(nextProps); download_path: string,
} completed: boolean,
},
restartDownload(props) { loading: boolean,
const { downloading, fileInfo, uri, restartDownload } = props; costInfo: ?{},
restartDownload: (string, number) => void,
openInShell: string => void,
purchaseUri: string => void,
doPause: () => void,
};
class FileDownloadLink extends React.PureComponent<Props> {
componentWillUpdate() {
const { downloading, fileInfo, uri, restartDownload } = this.props;
if ( if (
!downloading && !downloading &&
fileInfo && fileInfo &&
@ -27,12 +35,7 @@ class FileDownloadLink extends React.PureComponent {
} }
} }
checkAvailability(uri) { uri: ?string;
if (!this._uri || uri !== this._uri) {
this._uri = uri;
this.props.checkAvailability(uri);
}
}
render() { render() {
const { const {
@ -47,8 +50,10 @@ class FileDownloadLink extends React.PureComponent {
} = this.props; } = this.props;
const openFile = () => { const openFile = () => {
openInShell(fileInfo.download_path); if (fileInfo) {
doPause(); openInShell(fileInfo.download_path);
doPause();
}
}; };
if (loading || downloading) { if (loading || downloading) {
@ -56,21 +61,11 @@ class FileDownloadLink extends React.PureComponent {
fileInfo && fileInfo.written_bytes fileInfo && fileInfo.written_bytes
? fileInfo.written_bytes / fileInfo.total_bytes * 100 ? fileInfo.written_bytes / fileInfo.total_bytes * 100
: 0; : 0;
const label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...'); const label = fileInfo
? __('Downloading: ') + progress.toFixed(0) + __('% complete')
: __('Connecting...');
return ( return <span className="file-download">{label}</span>;
<div className="file-download btn__content">
<div
className={classnames('file-download__overlay', {
btn__content: !!progress,
})}
style={{ width: `${progress}%` }}
>
{label}
</div>
{label}
</div>
);
} else if (fileInfo === null && !downloading) { } else if (fileInfo === null && !downloading) {
if (!costInfo) { if (!costInfo) {
return null; return null;
@ -78,9 +73,10 @@ class FileDownloadLink extends React.PureComponent {
return ( return (
<Button <Button
className="btn--file-actions" button="alt"
description={__('Download')} label={__('Download')}
icon={icons.DOWNLOAD} icon={icons.DOWNLOAD}
iconColor="purple"
onClick={() => { onClick={() => {
purchaseUri(uri); purchaseUri(uri);
}} }}
@ -89,8 +85,9 @@ class FileDownloadLink extends React.PureComponent {
} else if (fileInfo && fileInfo.download_path) { } else if (fileInfo && fileInfo.download_path) {
return ( return (
<Button <Button
className="btn--file-actions" button="alt"
description={__('Open')} iconColor="purple"
label={__('Open File')}
icon={icons.OPEN} icon={icons.OPEN}
onClick={() => openFile()} onClick={() => openFile()}
/> />

View file

@ -8,6 +8,7 @@ type FileInfo = {
name: string, name: string,
channelName: ?string, channelName: ?string,
pending?: boolean, pending?: boolean,
channel_claim_id: string,
value?: { value?: {
publisherSignature: { publisherSignature: {
certificateId: string, certificateId: string,
@ -139,6 +140,8 @@ class FileList extends React.PureComponent<Props, State> {
}); });
} }
sortFunctions: {};
render() { render() {
const { fileInfos, hideFilter, checkPending } = this.props; const { fileInfos, hideFilter, checkPending } = this.props;
const { sortBy } = this.state; const { sortBy } = this.state;
@ -149,27 +152,14 @@ class FileList extends React.PureComponent<Props, State> {
} }
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => { this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const { const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
channel_name: channelName,
name: claimName,
claim_name: claimNameDownloaded,
claim_id: claimId,
} = fileInfo;
const uriParams = {}; const uriParams = {};
// This is unfortunate // This is unfortunate
// https://github.com/lbryio/lbry/issues/1159 // https://github.com/lbryio/lbry/issues/1159
const name = claimName || claimNameDownloaded; const name = claimName || claimNameDownloaded;
uriParams.contentName = name;
if (channelName) { uriParams.claimId = claimId;
uriParams.channelName = channelName;
uriParams.contentName = name;
uriParams.claimId = this.getChannelSignature(fileInfo);
} else {
uriParams.claimId = claimId;
uriParams.claimName = name;
}
const uri = buildURI(uriParams); const uri = buildURI(uriParams);
content.push(<FileCard key={uri} uri={uri} checkPending={checkPending} />); content.push(<FileCard key={uri} uri={uri} checkPending={checkPending} />);

View file

@ -35,20 +35,14 @@ class FilePrice extends React.PureComponent<Props> {
render() { render() {
const { costInfo, showFullPrice } = this.props; const { costInfo, showFullPrice } = this.props;
const isEstimate = costInfo ? !costInfo.includesData : false; return costInfo ? (
if (!costInfo) {
return <span className="credit-amount">PRICE</span>;
}
return (
<CreditAmount <CreditAmount
amount={costInfo.cost} amount={costInfo.cost}
isEstimate={isEstimate} isEstimate={!costInfo.includesData}
showFree showFree
showFullPrice={showFullPrice} showFullPrice={showFullPrice}
/> />
); ) : null;
} }
} }

View file

@ -1,26 +1,17 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import InviteNew from './view';
import { import {
selectUserInvitesRemaining, selectUserInvitesRemaining,
selectUserInviteNewIsPending, selectUserInviteNewIsPending,
selectUserInviteNewErrorMessage, selectUserInviteNewErrorMessage,
} from 'redux/selectors/user'; } from 'redux/selectors/user';
import rewards from 'rewards';
import { makeSelectRewardAmountByType } from 'redux/selectors/rewards';
import { doUserInviteNew } from 'redux/actions/user'; import { doUserInviteNew } from 'redux/actions/user';
import InviteNew from './view';
const select = state => { const select = state => ({
const selectReward = makeSelectRewardAmountByType(); errorMessage: selectUserInviteNewErrorMessage(state),
invitesRemaining: selectUserInvitesRemaining(state),
return { isPending: selectUserInviteNewIsPending(state),
errorMessage: selectUserInviteNewErrorMessage(state), });
invitesRemaining: selectUserInvitesRemaining(state),
isPending: selectUserInviteNewIsPending(state),
rewardAmount: selectReward(state, { reward_type: rewards.TYPE_REFERRAL }),
};
};
const perform = dispatch => ({ const perform = dispatch => ({
inviteNew: email => dispatch(doUserInviteNew(email)), inviteNew: email => dispatch(doUserInviteNew(email)),

View file

@ -29,8 +29,7 @@ class FormInviteNew extends React.PureComponent {
} }
render() { render() {
const { errorMessage, isPending, rewardAmount } = this.props; const { errorMessage, isPending } = this.props;
const label = `${__('Get')} ${rewardAmount} LBC`;
return ( return (
<Form onSubmit={this.handleSubmit}> <Form onSubmit={this.handleSubmit}>
@ -49,7 +48,7 @@ class FormInviteNew extends React.PureComponent {
/> />
</FormRow> </FormRow>
<div className="card__actions"> <div className="card__actions">
<Submit label={label} disabled={isPending} /> <Submit label="Invite" disabled={isPending} />
</div> </div>
</Form> </Form>
); );

View file

@ -29,6 +29,8 @@ type Props = {
currency: string, currency: string,
}, },
channel: string, channel: string,
channelId: ?string,
myChannels: Array<{ name: string }>,
name: ?string, name: ?string,
tosAccepted: boolean, tosAccepted: boolean,
updatePublishForm: UpdatePublishFormData => void, updatePublishForm: UpdatePublishFormData => void,
@ -137,13 +139,14 @@ class PublishForm extends React.PureComponent<Props> {
} }
handleChannelChange(channelName: string) { handleChannelChange(channelName: string) {
const { name, updatePublishForm } = this.props; const { name, updatePublishForm, myChannels } = this.props;
const form = { channel: channelName };
const namedChannelClaim = myChannels.find(channel => channel.name === channelName);
form.channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
if (name) { if (name) {
const uri = this.getNewUri(name, channelName); form.uri = this.getNewUri(name, channelName);
updatePublishForm({ channel: channelName, uri });
} else {
updatePublishForm({ channel: channelName });
} }
updatePublishForm(form);
} }
handleBidChange(bid: number) { handleBidChange(bid: number) {
@ -183,7 +186,7 @@ class PublishForm extends React.PureComponent<Props> {
description, description,
language, language,
nsfw, nsfw,
channel, channelId,
licenseType, licenseType,
licenseUrl, licenseUrl,
otherLicenseDescription, otherLicenseDescription,
@ -217,7 +220,7 @@ class PublishForm extends React.PureComponent<Props> {
description, description,
language, language,
nsfw, nsfw,
channel, channelId,
license: publishingLicense, license: publishingLicense,
licenseUrl: publishingLicenseUrl, licenseUrl: publishingLicenseUrl,
otherLicenseDescription, otherLicenseDescription,

View file

@ -16,7 +16,7 @@ const makeSelect = () => {
const select = (state, props) => ({ const select = (state, props) => ({
errorMessage: selectError(state, props), errorMessage: selectError(state, props),
isPending: selectIsPending(state, props), isPending: selectIsPending(state, props),
reward: selectReward(state, props), reward: selectReward(state, props.reward_type),
}); });
return select; return select;

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectCurrentPage, selectCurrentParams } from 'lbry-redux'; import { selectCurrentPage, selectCurrentParams, doNotify } from 'lbry-redux';
import Router from './view'; import Router from './view';
const select = state => ({ const select = state => ({
@ -7,4 +7,4 @@ const select = state => ({
currentPage: selectCurrentPage(state), currentPage: selectCurrentPage(state),
}); });
export default connect(select, null)(Router); export default connect(select, { doNotify })(Router);

View file

@ -18,16 +18,21 @@ import InvitePage from 'page/invite';
import BackupPage from 'page/backup'; import BackupPage from 'page/backup';
import SubscriptionsPage from 'page/subscriptions'; import SubscriptionsPage from 'page/subscriptions';
const route = (page, routesMap) => { const route = (props, page, routesMap) => {
const component = routesMap[page]; const component = routesMap[page];
if (!component) {
return component || DiscoverPage; props.doNotify({
message: __('Invalid page requested'),
displayType: ['snackbar'],
});
}
return component || routesMap.discover;
}; };
const Router = props => { const Router = props => {
const { currentPage, params } = props; const { currentPage, params } = props;
return route(currentPage, { return route(props, currentPage, {
auth: <AuthPage params={params} />, auth: <AuthPage params={params} />,
backup: <BackupPage params={params} />, backup: <BackupPage params={params} />,
channel: <ChannelPage params={params} />, channel: <ChannelPage params={params} />,

View file

@ -37,8 +37,9 @@ export default (props: Props) => {
return channelName && uri ? ( return channelName && uri ? (
<Button <Button
iconColor="red"
icon={isSubscribed ? undefined : icons.HEART} icon={isSubscribed ? undefined : icons.HEART}
button={isSubscribed ? 'danger' : 'alt'} button="alt"
label={subscriptionLabel} label={subscriptionLabel}
onClick={() => { onClick={() => {
if (!subscriptions.length) { if (!subscriptions.length) {

View file

@ -1,4 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doNotify, MODALS } from 'lbry-redux';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { doUserIdentityVerify } from 'redux/actions/user'; import { doUserIdentityVerify } from 'redux/actions/user';
import rewards from 'rewards'; import rewards from 'rewards';
@ -8,15 +9,14 @@ import {
selectIdentityVerifyErrorMessage, selectIdentityVerifyErrorMessage,
} from 'redux/selectors/user'; } from 'redux/selectors/user';
import UserVerify from './view'; import UserVerify from './view';
import { doNotify, MODALS } from 'lbry-redux';
const select = (state, props) => { const select = state => {
const selectReward = makeSelectRewardByType(); const selectReward = makeSelectRewardByType();
return { return {
isPending: selectIdentityVerifyIsPending(state), isPending: selectIdentityVerifyIsPending(state),
errorMessage: selectIdentityVerifyErrorMessage(state), errorMessage: selectIdentityVerifyErrorMessage(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), reward: selectReward(state, rewards.TYPE_NEW_USER),
}; };
}; };

View file

@ -18,7 +18,7 @@ class VideoPlayButton extends React.PureComponent<Props> {
const label = doesPlayback ? 'Play' : 'View'; const label = doesPlayback ? 'Play' : 'View';
return ( return (
<Button button="secondary" disabled={disabled} label={label} icon={icon} onClick={play} /> <Button button="primary" disabled={disabled} label={label} icon={icon} onClick={play} />
); );
} }
} }

View file

@ -0,0 +1,3 @@
import ViewOnWebButton from './view';
export default ViewOnWebButton;

View file

@ -0,0 +1,21 @@
// @flow
import React from 'react';
import * as icons from 'constants/icons';
import Button from 'component/button';
type Props = {
uri: ?string,
};
export default (props: Props) => {
const { uri } = props;
return uri ? (
<Button
iconRight={icons.GLOBE}
button="alt"
label={__('View on Web')}
href={`http://spee.ch/${uri}`}
/>
) : null;
};

View file

@ -25,3 +25,4 @@ export const CHECK = 'CheckCircle';
export const HEART = 'Heart'; export const HEART = 'Heart';
export const UNLOCK = 'Unlock'; export const UNLOCK = 'Unlock';
export const CHECK_SIMPLE = 'Check'; export const CHECK_SIMPLE = 'Check';
export const GLOBE = 'Globe';

View file

@ -8,7 +8,7 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate } from 'redux/actions/app'; import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate } from 'redux/actions/app';
import { doNotify, doBlackListedOutpointsSubscribe } from 'lbry-redux'; import { doNotify, doBlackListedOutpointsSubscribe, isURIValid } from 'lbry-redux';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings'; import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings';
import { doUserEmailVerify } from 'redux/actions/user'; import { doUserEmailVerify } from 'redux/actions/user';
@ -17,8 +17,10 @@ import store from 'store';
import app from './app'; import app from './app';
import analytics from './analytics'; import analytics from './analytics';
import doLogWarningConsoleMessage from './logWarningConsoleMessage'; import doLogWarningConsoleMessage from './logWarningConsoleMessage';
import { initContextMenu } from './util/contextMenu';
const { autoUpdater } = remote.require('electron-updater'); const { autoUpdater } = remote.require('electron-updater');
const APPPAGEURL = 'lbry://?';
autoUpdater.logger = remote.require('electron-log'); autoUpdater.logger = remote.require('electron-log');
@ -42,8 +44,18 @@ ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
}) })
); );
} }
} else { } else if (uri.startsWith(APPPAGEURL)) {
const navpage = uri.replace(APPPAGEURL, '').toLowerCase();
app.store.dispatch(doNavigate(`/${navpage}`));
} else if (isURIValid(uri)) {
app.store.dispatch(doNavigate('/show', { uri })); app.store.dispatch(doNavigate('/show', { uri }));
} else {
app.store.dispatch(
doNotify({
message: __('Invalid LBRY URL requested'),
displayType: ['snackbar'],
})
);
} }
} }
}); });
@ -101,6 +113,8 @@ document.addEventListener('click', event => {
} }
}); });
document.addEventListener('contextmenu', initContextMenu);
const init = () => { const init = () => {
autoUpdater.on('update-downloaded', () => { autoUpdater.on('update-downloaded', () => {
app.store.dispatch(doAutoUpdate()); app.store.dispatch(doAutoUpdate());
@ -135,7 +149,7 @@ const init = () => {
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<div> <div>
<App /> <App onContextMenu={e => openContextMenu(e)} />
<SnackBar /> <SnackBar />
</div> </div>
</Provider>, </Provider>,

View file

@ -25,16 +25,13 @@ const ModalCreditIntro = props => {
can take are limited. can take are limited.
</p> </p>
)} )}
<p> {totalRewardValue && (
There are a variety of ways to get credits, including more than{' '} <p>
{totalRewardValue ? ( There are a variety of ways to get credits, including more than{' '}
<CreditAmount noStyle amount={totalRewardRounded} /> <CreditAmount noStyle amount={totalRewardRounded} />{' '}
) : ( {__('in free rewards for participating in the LBRY beta.')}
<span className="credit-amount">{__('?? credits')}</span> </p>
)}{' '} )}
{__(' in free rewards for participating in the LBRY beta.')}
</p>
<div className="card__actions card__actions--center"> <div className="card__actions card__actions--center">
<Button button="primary" onClick={addBalance} label={__('Get Credits')} /> <Button button="primary" onClick={addBalance} label={__('Get Credits')} />
<Button <Button

View file

@ -8,7 +8,7 @@ const select = state => {
const selectReward = makeSelectRewardByType(); const selectReward = makeSelectRewardByType();
return { return {
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), reward: selectReward(state, rewards.TYPE_NEW_USER),
}; };
}; };

View file

@ -6,7 +6,7 @@ import CreditAmount from 'component/common/credit-amount';
class ModalFirstReward extends React.PureComponent { class ModalFirstReward extends React.PureComponent {
render() { render() {
const { closeModal, reward } = this.props; const { closeModal } = this.props;
return ( return (
<Modal <Modal
@ -18,10 +18,7 @@ class ModalFirstReward extends React.PureComponent {
> >
<section> <section>
<h3 className="modal__header">{__('Your First Reward')}</h3> <h3 className="modal__header">{__('Your First Reward')}</h3>
<p> <p>{__('You just earned your first reward!')}</p>
{__('You just earned your first reward of')}{' '}
<CreditAmount amount={reward.reward_amount} />.
</p>
<p> <p>
{__( {__(
"This reward will show in your Wallet in the top right momentarily (if it hasn't already)." "This reward will show in your Wallet in the top right momentarily (if it hasn't already)."

View file

@ -1,12 +1,15 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import BusyIndicator from 'component/common/busy-indicator'; import BusyIndicator from 'component/common/busy-indicator';
import Button from 'component/button';
import { FormField, FormRow } from 'component/common/form'; import { FormField, FormRow } from 'component/common/form';
import ReactPaginate from 'react-paginate'; import ReactPaginate from 'react-paginate';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import ViewOnWebButton from 'component/viewOnWebButton';
import Page from 'component/page'; import Page from 'component/page';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import { MODALS } from 'lbry-redux';
type Props = { type Props = {
uri: string, uri: string,
@ -19,6 +22,7 @@ type Props = {
fetchClaims: (string, number) => void, fetchClaims: (string, number) => void,
fetchClaimCount: string => void, fetchClaimCount: string => void,
navigate: (string, {}) => void, navigate: (string, {}) => void,
openModal: (string, {}) => void,
}; };
class ChannelPage extends React.PureComponent<Props> { class ChannelPage extends React.PureComponent<Props> {
@ -56,8 +60,8 @@ class ChannelPage extends React.PureComponent<Props> {
} }
render() { render() {
const { fetching, claimsInChannel, claim, page, totalPages } = this.props; const { fetching, claimsInChannel, claim, page, totalPages, uri, openModal } = this.props;
const { name, permanent_url: permanentUrl } = claim; const { name, permanent_url: permanentUrl, claim_id: claimId } = claim;
let contentList; let contentList;
if (fetching) { if (fetching) {
@ -76,7 +80,14 @@ class ChannelPage extends React.PureComponent<Props> {
<section className="card__channel-info card__channel-info--large"> <section className="card__channel-info card__channel-info--large">
<h1>{name}</h1> <h1>{name}</h1>
<div className="card__actions card__actions--no-margin"> <div className="card__actions card__actions--no-margin">
<Button
button="alt"
iconRight="Send"
label={__('Enjoy this? Send a tip')}
onClick={() => openModal(MODALS.SEND_TIP, { uri })}
/>
<SubscribeButton uri={permanentUrl} channelName={name} /> <SubscribeButton uri={permanentUrl} channelName={name} />
<ViewOnWebButton uri={`${name}:${claimId}`} />
</div> </div>
</section> </section>
<section>{contentList}</section> <section>{contentList}</section>

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Page from 'component/page'; import Page from 'component/page';
import CategoryList from 'component/common/category-list'; import CategoryList from 'component/categoryList';
type Props = { type Props = {
fetchFeaturedUris: () => void, fetchFeaturedUris: () => void,
@ -27,7 +27,11 @@ class DiscoverPage extends React.PureComponent<Props> {
featuredUris[category].length ? ( featuredUris[category].length ? (
<CategoryList key={category} category={category} names={featuredUris[category]} /> <CategoryList key={category} category={category} names={featuredUris[category]} />
) : ( ) : (
'' <CategoryList
key={category}
category={category.split('#')[0]}
categoryLink={category}
/>
) )
)} )}
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>} {failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}

View file

@ -13,10 +13,13 @@ import DateTime from 'component/dateTime';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import Button from 'component/button'; import Button from 'component/button';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import ViewOnWebButton from 'component/viewOnWebButton';
import Page from 'component/page'; import Page from 'component/page';
import player from 'render-media'; import player from 'render-media';
import * as settings from 'constants/settings'; import * as settings from 'constants/settings';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import type { Subscription } from 'types/subscription';
import FileDownloadLink from 'component/fileDownloadLink';
type Props = { type Props = {
claim: Claim, claim: Claim,
@ -39,10 +42,10 @@ type Props = {
openModal: ({ id: string }, { uri: string }) => void, openModal: ({ id: string }, { uri: string }) => void,
fetchFileInfo: string => void, fetchFileInfo: string => void,
fetchCostInfo: string => void, fetchCostInfo: string => void,
prepareEdit: ({}) => void, prepareEdit: ({}, string) => void,
setClientSetting: (string, boolean | string) => void, setClientSetting: (string, boolean | string) => void,
checkSubscription: ({ channelName: string, uri: string }) => void, checkSubscription: ({ channelName: string, uri: string }) => void,
subscriptions: Array<{}>, subscriptions: Array<Subscription>,
}; };
class FilePage extends React.Component<Props> { class FilePage extends React.Component<Props> {
@ -78,12 +81,7 @@ class FilePage extends React.Component<Props> {
} }
checkSubscription = (props: Props) => { checkSubscription = (props: Props) => {
if ( if (props.subscriptions.find(sub => sub.channelName === props.claim.channel_name)) {
props.claim.value.publisherSignature &&
props.subscriptions
.map(subscription => subscription.channelName)
.indexOf(props.claim.channel_name) !== -1
) {
props.checkSubscription({ props.checkSubscription({
channelName: props.claim.channel_name, channelName: props.claim.channel_name,
uri: buildURI( uri: buildURI(
@ -112,6 +110,7 @@ class FilePage extends React.Component<Props> {
prepareEdit, prepareEdit,
navigate, navigate,
autoplay, autoplay,
costInfo,
} = this.props; } = this.props;
// File info // File info
@ -128,6 +127,19 @@ class FilePage extends React.Component<Props> {
if (channelName && channelClaimId) { if (channelName && channelClaimId) {
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false); subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
} }
const speechSharable =
costInfo &&
costInfo.cost === 0 &&
contentType &&
['video', 'image'].includes(contentType.split('/')[0]);
// We want to use the short form uri for editing
// This is what the user is used to seeing, they don't care about the claim id
// We will select the claim id before they publish
let editUri;
if (claimIsMine) {
editUri = buildURI({ channelName, contentName: claim.name });
}
const isPlaying = playingUri === uri && !isPaused; const isPlaying = playingUri === uri && !isPaused;
return ( return (
@ -143,17 +155,12 @@ class FilePage extends React.Component<Props> {
) : ( ) : (
<Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} /> <Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} />
)} )}
{!isPlaying && (
<div className="card-media__internal-links">
<FileActions uri={uri} vertical />
</div>
)}
<div className="card__content"> <div className="card__content">
<div className="card__title-identity--file"> <div className="card__title-identity--file">
<h1 className="card__title card__title--file">{title}</h1> <h1 className="card__title card__title--file">{title}</h1>
<div className="card__title-identity-icons"> <div className="card__title-identity-icons">
<FilePrice uri={normalizeURI(uri)} /> <FilePrice uri={normalizeURI(uri)} />
{isRewardContent && <Icon icon={icons.FEATURED} />} {isRewardContent && <Icon iconColor="red" tooltip="bottom" icon={icons.FEATURED} />}
</div> </div>
</div> </div>
<span className="card__subtitle card__subtitle--file"> <span className="card__subtitle card__subtitle--file">
@ -163,6 +170,7 @@ class FilePage extends React.Component<Props> {
{metadata.nsfw && <div>NSFW</div>} {metadata.nsfw && <div>NSFW</div>}
<div className="card__channel-info"> <div className="card__channel-info">
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
<div className="card__actions card__actions--no-margin"> <div className="card__actions card__actions--no-margin">
{claimIsMine ? ( {claimIsMine ? (
<Button <Button
@ -170,7 +178,7 @@ class FilePage extends React.Component<Props> {
icon={icons.EDIT} icon={icons.EDIT}
label={__('Edit')} label={__('Edit')}
onClick={() => { onClick={() => {
prepareEdit(claim, uri); prepareEdit(claim, editUri);
navigate('/publish'); navigate('/publish');
}} }}
/> />
@ -185,6 +193,14 @@ class FilePage extends React.Component<Props> {
<SubscribeButton uri={subscriptionUri} channelName={channelName} /> <SubscribeButton uri={subscriptionUri} channelName={channelName} />
</React.Fragment> </React.Fragment>
)} )}
{speechSharable && (
<ViewOnWebButton
uri={buildURI({
claimId: claim.claim_id,
contentName: claim.name,
}).slice(7)}
/>
)}
</div> </div>
</div> </div>
<FormRow alignRight> <FormRow alignRight>
@ -199,6 +215,11 @@ class FilePage extends React.Component<Props> {
</div> </div>
<div className="card__content"> <div className="card__content">
<FileDownloadLink uri={uri} />
<FileActions uri={uri} />
</div>
<div className="card__content--extra-padding">
<FileDetails uri={uri} /> <FileDetails uri={uri} />
</div> </div>
</section> </section>

View file

@ -35,6 +35,7 @@ const select = (state, props) => {
const claimsByUri = selectClaimsByUri(state); const claimsByUri = selectClaimsByUri(state);
const myClaims = selectMyClaims(state); const myClaims = selectMyClaims(state);
const myChannels = selectMyChannelClaims(state);
const claimForUri = claimsByUri[uri]; const claimForUri = claimsByUri[uri];
let winningBidForClaimUri; let winningBidForClaimUri;
@ -50,6 +51,7 @@ const select = (state, props) => {
claimForUri, claimForUri,
winningBidForClaimUri, winningBidForClaimUri,
myClaimForUri, myClaimForUri,
myChannels,
costInfo: makeSelectCostInfoForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state),
balance: selectBalance(state), balance: selectBalance(state),
}; };

View file

@ -5,6 +5,8 @@ import FileTile from 'component/fileTile';
import FileListSearch from 'component/fileListSearch'; import FileListSearch from 'component/fileListSearch';
import ToolTip from 'component/common/tooltip'; import ToolTip from 'component/common/tooltip';
import Page from 'component/page'; import Page from 'component/page';
import Icon from 'component/common/icon';
import * as icons from 'constants/icons';
const MODAL_ANIMATION_TIME = 250; const MODAL_ANIMATION_TIME = 250;
@ -50,10 +52,11 @@ class SearchPage extends React.PureComponent<Props> {
<div className="file-list__header"> <div className="file-list__header">
{__('Exact URL')} {__('Exact URL')}
<ToolTip <ToolTip
label="?" icon
body={__('This is the resolution of a LBRY URL and not controlled by LBRY Inc.')} body={__('This is the resolution of a LBRY URL and not controlled by LBRY Inc.')}
className="tooltip--header" >
/> <Icon icon={icons.HELP} />
</ToolTip>
</div> </div>
<FileTile fullWidth uri={normalizeURI(query)} showUri /> <FileTile fullWidth uri={normalizeURI(query)} showUri />
</React.Fragment> </React.Fragment>

View file

@ -42,10 +42,10 @@ export function doFetchFeaturedUris() {
}); });
const success = ({ Uris }) => { const success = ({ Uris }) => {
let urisToResolve = []; const urisToResolve = Object.keys(Uris).reduce(
Object.keys(Uris).forEach(category => { (resolve, category) => [...resolve, ...Uris[category]],
urisToResolve = [...urisToResolve, ...Uris[category]]; []
}); );
const actions = [ const actions = [
doResolveUris(urisToResolve), doResolveUris(urisToResolve),

View file

@ -6,7 +6,6 @@ import type {
UpdatePublishFormAction, UpdatePublishFormAction,
PublishParams, PublishParams,
} from 'redux/reducers/publish'; } from 'redux/reducers/publish';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH }; type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH };
type PromiseAction = Promise<Action>; type PromiseAction = Promise<Action>;
@ -86,6 +85,7 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
thumbnail, thumbnail,
nsfw, nsfw,
channel, channel,
channelId,
title, title,
contentIsFree, contentIsFree,
price, price,
@ -104,7 +104,6 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
} }
} }
const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel;
const fee = contentIsFree || !price.amount ? undefined : { ...price }; const fee = contentIsFree || !price.amount ? undefined : { ...price };
const metadata = { const metadata = {
@ -126,7 +125,7 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
const publishPayload = { const publishPayload = {
name, name,
channel_name: channelName, channel_id: channelId,
bid, bid,
metadata, metadata,
}; };

View file

@ -1,7 +1,7 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
import { doNotify, MODALS } from 'lbry-redux'; import { doNotify, MODALS } from 'lbry-redux';
import { selectUnclaimedRewardsByType } from 'redux/selectors/rewards'; import { selectUnclaimedRewards } from 'redux/selectors/rewards';
import { selectUserIsRewardApproved } from 'redux/selectors/user'; import { selectUserIsRewardApproved } from 'redux/selectors/user';
import rewards from 'rewards'; import rewards from 'rewards';
@ -30,8 +30,8 @@ export function doRewardList() {
export function doClaimRewardType(rewardType) { export function doClaimRewardType(rewardType) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const rewardsByType = selectUnclaimedRewardsByType(state); const unclaimedRewards = selectUnclaimedRewards(state);
const reward = rewardsByType[rewardType]; const reward = unclaimedRewards.find(ur => ur.reward_type === rewardType);
const userIsRewardApproved = selectUserIsRewardApproved(state); const userIsRewardApproved = selectUserIsRewardApproved(state);
if (!reward || reward.transaction_id) { if (!reward || reward.transaction_id) {
@ -84,14 +84,14 @@ export function doClaimRewardType(rewardType) {
export function doClaimEligiblePurchaseRewards() { export function doClaimEligiblePurchaseRewards() {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const rewardsByType = selectUnclaimedRewardsByType(state); const unclaimedRewards = selectUnclaimedRewards(state);
const userIsRewardApproved = selectUserIsRewardApproved(state); const userIsRewardApproved = selectUserIsRewardApproved(state);
if (!userIsRewardApproved || !Lbryio.enabled) { if (!userIsRewardApproved || !Lbryio.enabled) {
return; return;
} }
if (rewardsByType[rewards.TYPE_FIRST_STREAM]) { if (unclaimedRewards.find(ur => ur.reward_type === rewards.TYPE_FIRST_STREAM)) {
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM)); dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
} else { } else {
[rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_FEATURED_DOWNLOAD].forEach(type => { [rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_FEATURED_DOWNLOAD].forEach(type => {

View file

@ -18,6 +18,7 @@ type PublishState = {
language: string, language: string,
tosAccepted: boolean, tosAccepted: boolean,
channel: string, channel: string,
channelId: ?string,
name: string, name: string,
nameError: ?string, nameError: ?string,
bid: number, bid: number,
@ -41,6 +42,7 @@ export type UpdatePublishFormData = {
language?: string, language?: string,
tosAccepted?: boolean, tosAccepted?: boolean,
channel?: string, channel?: string,
channelId?: string,
name?: string, name?: string,
nameError?: string, nameError?: string,
bid?: number, bid?: number,
@ -66,6 +68,7 @@ export type PublishParams = {
thumbnail: ?string, thumbnail: ?string,
nsfw: boolean, nsfw: boolean,
channel: string, channel: string,
channelId: string,
title: string, title: string,
contentIsFree: boolean, contentIsFree: boolean,
uri: string, uri: string,
@ -97,6 +100,7 @@ const defaultState: PublishState = {
language: 'en', language: 'en',
nsfw: false, nsfw: false,
channel: CHANNEL_ANONYMOUS, channel: CHANNEL_ANONYMOUS,
channelId: '',
tosAccepted: false, tosAccepted: false,
name: '', name: '',
nameError: undefined, nameError: undefined,

View file

@ -4,7 +4,7 @@ const reducers = {};
const defaultState = { const defaultState = {
fetching: false, fetching: false,
claimedRewardsById: {}, // id => reward claimedRewardsById: {}, // id => reward
unclaimedRewardsByType: {}, unclaimedRewards: [],
claimPendingByType: {}, claimPendingByType: {},
claimErrorsByType: {}, claimErrorsByType: {},
}; };
@ -17,19 +17,19 @@ reducers[ACTIONS.FETCH_REWARDS_STARTED] = state =>
reducers[ACTIONS.FETCH_REWARDS_COMPLETED] = (state, action) => { reducers[ACTIONS.FETCH_REWARDS_COMPLETED] = (state, action) => {
const { userRewards } = action.data; const { userRewards } = action.data;
const unclaimedRewards = {}; const unclaimedRewards = [];
const claimedRewards = {}; const claimedRewards = {};
userRewards.forEach(reward => { userRewards.forEach(reward => {
if (reward.transaction_id) { if (reward.transaction_id) {
claimedRewards[reward.id] = reward; claimedRewards[reward.id] = reward;
} else { } else {
unclaimedRewards[reward.reward_type] = reward; unclaimedRewards.push(reward);
} }
}); });
return Object.assign({}, state, { return Object.assign({}, state, {
claimedRewardsById: claimedRewards, claimedRewardsById: claimedRewards,
unclaimedRewardsByType: unclaimedRewards, unclaimedRewards,
fetching: false, fetching: false,
}); });
}; };
@ -62,24 +62,21 @@ reducers[ACTIONS.CLAIM_REWARD_STARTED] = (state, action) => {
reducers[ACTIONS.CLAIM_REWARD_SUCCESS] = (state, action) => { reducers[ACTIONS.CLAIM_REWARD_SUCCESS] = (state, action) => {
const { reward } = action.data; const { reward } = action.data;
const { unclaimedRewards } = state;
const unclaimedRewardsByType = Object.assign({}, state.unclaimedRewardsByType); const index = unclaimedRewards.findIndex(ur => ur.reward_type === reward.reward_type);
const existingReward = unclaimedRewardsByType[reward.reward_type]; unclaimedRewards.splice(index, 1);
const newReward = Object.assign({}, reward, { const { claimedRewardsById } = state;
reward_title: existingReward.reward_title, claimedRewardsById[reward.id] = reward;
reward_description: existingReward.reward_description,
});
const claimedRewardsById = Object.assign({}, state.claimedRewardsById); const newState = {
claimedRewardsById[reward.id] = newReward; ...state,
unclaimedRewards: [...unclaimedRewards],
claimedRewardsById: { ...claimedRewardsById },
};
const newState = Object.assign({}, state, { return setClaimRewardState(newState, reward, false, '');
unclaimedRewardsByType,
claimedRewardsById,
});
return setClaimRewardState(newState, newReward, false, '');
}; };
reducers[ACTIONS.CLAIM_REWARD_FAILURE] = (state, action) => { reducers[ACTIONS.CLAIM_REWARD_FAILURE] = (state, action) => {

View file

@ -1,5 +1,4 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import REWARDS from 'rewards';
const selectState = state => state.rewards || {}; const selectState = state => state.rewards || {};
@ -26,16 +25,7 @@ export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedR
}, {}) }, {})
); );
export const selectUnclaimedRewards = createSelector( export const selectUnclaimedRewards = createSelector(selectState, state => state.unclaimedRewards);
selectUnclaimedRewardsByType,
byType =>
Object.values(byType).sort(
(a, b) =>
REWARDS.SORT_ORDER.indexOf(a.reward_type) < REWARDS.SORT_ORDER.indexOf(b.reward_type)
? -1
: 1
) || []
);
export const selectFetchingRewards = createSelector(selectState, state => !!state.fetching); export const selectFetchingRewards = createSelector(selectState, state => !!state.fetching);
@ -65,7 +55,8 @@ const selectClaimRewardError = (state, props) =>
export const makeSelectClaimRewardError = () => export const makeSelectClaimRewardError = () =>
createSelector(selectClaimRewardError, errorMessage => errorMessage); createSelector(selectClaimRewardError, errorMessage => errorMessage);
const selectRewardByType = (state, props) => selectUnclaimedRewardsByType(state)[props.reward_type]; const selectRewardByType = (state, rewardType) =>
selectUnclaimedRewards(state).find(reward => reward.reward_type === rewardType);
export const makeSelectRewardByType = () => createSelector(selectRewardByType, reward => reward); export const makeSelectRewardByType = () => createSelector(selectRewardByType, reward => reward);

View file

@ -1,21 +1,6 @@
import { Lbry, doNotify } from 'lbry-redux'; import { Lbry, doNotify } from 'lbry-redux';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
function rewardMessage(type, amount) {
return {
new_developer: __('You earned %s for registering as a new developer.', amount),
new_user: __('You earned %s LBC new user reward.', amount),
verified_email: __('You earned %s LBC for verifying your email address.', amount),
new_channel: __('You earned %s LBC for creating a publisher identity.', amount),
first_stream: __('You earned %s LBC for streaming your first video.', amount),
many_downloads: __('You earned %s LBC for downloading a bunch of things.', amount),
first_publish: __('You earned %s LBC for making your first publication.', amount),
featured_download: __('You earned %s LBC for watching a featured download.', amount),
referral: __('You earned %s LBC for referring someone.', amount),
youtube_creator: __('You earned %s LBC for syncing your YouTube channel.', amount),
}[type];
}
const rewards = {}; const rewards = {};
rewards.TYPE_NEW_DEVELOPER = 'new_developer'; rewards.TYPE_NEW_DEVELOPER = 'new_developer';
@ -28,18 +13,6 @@ rewards.TYPE_FIRST_PUBLISH = 'first_publish';
rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download'; rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download';
rewards.TYPE_REFERRAL = 'referral'; rewards.TYPE_REFERRAL = 'referral';
rewards.YOUTUBE_CREATOR = 'youtube_creator'; rewards.YOUTUBE_CREATOR = 'youtube_creator';
rewards.SORT_ORDER = [
rewards.TYPE_NEW_USER,
rewards.TYPE_CONFIRM_EMAIL,
rewards.TYPE_FIRST_STREAM,
rewards.TYPE_FIRST_CHANNEL,
rewards.TYPE_FIRST_PUBLISH,
rewards.TYPE_FEATURED_DOWNLOAD,
rewards.TYPE_MANY_DOWNLOADS,
rewards.TYPE_REFERRAL,
rewards.TYPE_NEW_DEVELOPER,
rewards.YOUTUBE_CREATOR,
];
rewards.claimReward = type => { rewards.claimReward = type => {
function requestReward(resolve, reject, params) { function requestReward(resolve, reject, params) {
@ -48,7 +21,8 @@ rewards.claimReward = type => {
return; return;
} }
Lbryio.call('reward', 'new', params, 'post').then(reward => { Lbryio.call('reward', 'new', params, 'post').then(reward => {
const message = rewardMessage(type, reward.reward_amount); const message =
reward.reward_notification || `You have claimed a ${reward.reward_amount} LBC reward.`;
// Display global notice // Display global notice
const action = doNotify({ const action = doNotify({

View file

@ -35,6 +35,7 @@ $large-breakpoint: 1760px;
--color-green-light: #effbe4; --color-green-light: #effbe4;
--color-green-blue: #2ec1a8; --color-green-blue: #2ec1a8;
--color-purple: #8165b0; --color-purple: #8165b0;
--color-blue-grey: #203049;
/* Colors */ /* Colors */
--color-divider: #e3e3e3; --color-divider: #e3e3e3;
@ -73,6 +74,8 @@ $large-breakpoint: 1760px;
--input-copyable-bg: #f6f6f6; --input-copyable-bg: #f6f6f6;
--input-copyable-color: var(--color-grey-dark); --input-copyable-color: var(--color-grey-dark);
--input-copyable-border: var(--color-grey); --input-copyable-border: var(--color-grey);
--input-select-bg-color: var(--color-grey);
--input-select-color: var(--text-color);
/* input:disabled */ /* input:disabled */
--input-disabled-border-color: rgba(0, 0, 0, 0.42); --input-disabled-border-color: rgba(0, 0, 0, 0.42);
@ -158,9 +161,8 @@ $large-breakpoint: 1760px;
--modal-btn-bg-color: var(--btn-bg-alt); --modal-btn-bg-color: var(--btn-bg-alt);
// /* Tooltip */ // /* Tooltip */
--tooltip-width: 300px; --tooltip-bg: #555;
--tooltip-bg: var(--color-bg); --tooltip-color: var(--color-white);
--tooltip-color: var(--text-color);
/* Scrollbar */ /* Scrollbar */
--scrollbar-radius: 10px; --scrollbar-radius: 10px;

View file

@ -2,7 +2,6 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
border-radius: var(--card-radius); border-radius: var(--card-radius);
overflow: auto;
user-select: text; user-select: text;
display: flex; display: flex;
position: relative; position: relative;
@ -18,7 +17,6 @@
.card--small { .card--small {
width: var(--card-small-width); width: var(--card-small-width);
overflow-x: hidden;
white-space: normal; white-space: normal;
.card__media { .card__media {
@ -87,8 +85,7 @@
align-items: center; align-items: center;
.credit-amount, .credit-amount,
.icon { .icon {
margin-top: $spacing-vertical * 1/3; margin: $spacing-vertical * 1/3;
margin-left: $spacing-vertical * 2/3;
} }
} }
@ -127,7 +124,11 @@
color: var(--card-text-color); color: var(--card-text-color);
.icon { .icon {
margin-left: $spacing-vertical * 1/3; margin-top: $spacing-vertical * 1/6;
&:not(:first-of-type) {
margin: 0 $spacing-vertical * 1/3;
}
} }
} }
@ -135,11 +136,6 @@
padding-top: $spacing-vertical * 1/3; padding-top: $spacing-vertical * 1/3;
} }
.card__subtitle--file-info {
display: flex;
align-items: center;
}
.card__subtitle--block { .card__subtitle--block {
display: block; display: block;
} }
@ -184,6 +180,10 @@
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
} }
.card__content--extra-padding {
margin-top: $spacing-vertical;
}
.card__subtext-title { .card__subtext-title {
color: var(--text-color); color: var(--text-color);
font-size: calc(var(--font-size-subtext-multiple) * 1.5em); font-size: calc(var(--font-size-subtext-multiple) * 1.5em);
@ -240,12 +240,11 @@
} }
/* /*
.card-row is used on the discover/subscriptions page .card-row is used on the discover page
It is a list of cards that extend past the right edge of the screen It is a list of cards that extend past the right edge of the screen
There are left/right arrows to scroll the cards and view hidden content There are left/right arrows to scroll the cards and view hidden content
*/ */
.card-row { .card-row {
overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
min-width: var(--card-small-width); min-width: var(--card-small-width);
@ -286,6 +285,18 @@
padding-top: $spacing-vertical * 2/3; padding-top: $spacing-vertical * 2/3;
overflow: hidden; overflow: hidden;
.card {
display: inline-block;
vertical-align: top;
overflow-y: visible;
// 31 px to handle to padding between cards
width: calc((100% / 4) - 31px);
}
.card:not(:first-of-type) {
margin-left: 20px;
}
.card:first-of-type { .card:first-of-type {
margin-left: $spacing-width; margin-left: $spacing-width;
} }
@ -324,27 +335,6 @@
} }
} }
.card-row__scrollhouse {
padding-top: $spacing-vertical * 2/3;
overflow: hidden;
.card {
display: inline-block;
vertical-align: top;
overflow: visible;
// 31 px to handle to padding between cards
width: calc((100% / 4) - 31px);
}
.card:not(:first-of-type) {
margin-left: 20px;
}
.card:last-of-type {
margin-right: 20px;
}
}
.card__success-msg { .card__success-msg {
border-left: 2px solid var(--success-msg-border); border-left: 2px solid var(--success-msg-border);
color: var(--success-msg-color); color: var(--success-msg-color);

View file

@ -1,26 +1,3 @@
.file-download,
.file-download__overlay {
padding: 5px;
}
.file-download { .file-download {
position: relative; font-size: 0.8em;
background-color: var(--color-black);
border-radius: var(--btn-radius);
color: var(--color-download);
font-size: 12px;
opacity: 0.8;
font-family: 'metropolis-medium';
}
.file-download__overlay {
background: var(--color-download);
color: var(--color-download-overlay);
border-radius: var(--btn-radius);
position: absolute;
white-space: nowrap;
overflow: hidden;
z-index: 1;
top: 0px;
left: 0px;
} }

View file

@ -14,6 +14,10 @@
.file-list__header { .file-list__header {
margin-top: $spacing-vertical * 4/3; margin-top: $spacing-vertical * 4/3;
font-size: 18px; font-size: 18px;
.tooltip {
margin-left: 5px;
}
} }
.file-tile { .file-tile {

View file

@ -62,6 +62,10 @@
width: 35px; width: 35px;
} }
input.paginate-channel {
width: 35px;
}
&.form-field--auto-height { &.form-field--auto-height {
height: auto; height: auto;
} }
@ -100,10 +104,23 @@
padding-left: $spacing-vertical * 1/3; padding-left: $spacing-vertical * 1/3;
} }
.form-field__select {
min-width: 60px;
height: 30px;
border-radius: 8px;
background-color: var(--input-select-bg-color);
font: normal 12px/30px 'metropolis-medium';
color: var(--input-select-color);
&:disabled {
opacity: 0.5;
}
}
// Not sure if I like these // Not sure if I like these
// Maybe this should be in gui.scss? // Maybe this should be in gui.scss?
.input--price-amount { .input--price-amount {
width: 60px; width: 80px;
} }
.input--address { .input--address {

View file

@ -1,30 +1,97 @@
@import '../mixin/link.scss';
.tooltip { .tooltip {
position: relative; position: relative;
padding: 0 $spacing-vertical / 3; display: inline-block;
font-size: 12px;
} }
.tooltip__body { // When there is a label for the tooltip and not just using a button or icon
.tooltip.tooltip--label {
font-size: 12px;
padding-left: $spacing-vertical * 1/3;
.tooltip__body {
margin-top: 5px;
}
}
.tooltip.tooltip--icon {
margin-top: 5px;
}
/* Tooltip text */
.tooltip .tooltip__body {
background-color: var(--tooltip-bg);
font-family: 'metropolis-medium';
font-size: 12px;
color: var(--tooltip-color);
border-radius: 8px;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
left: 50%; width: 200px;
margin-left: calc(var(--tooltip-width) * -1 / 2); text-align: center;
white-space: normal; white-space: pre-wrap;
box-sizing: border-box; padding: $spacing-vertical * 1/3;
padding: $spacing-vertical / 2; visibility: hidden;
width: var(--tooltip-width);
color: var(--tooltip-color);
background-color: var(--tooltip-bg);
font-size: calc(var(--font-size) * 7 / 8);
line-height: var(--font-line-height);
box-shadow: var(--box-shadow-layer);
border-radius: var(--card-radius);
} }
.tooltip__link { .tooltip .tooltip__body::after {
font-size: calc(var(--font-size) * 3 / 4); content: ' ';
margin-left: var(--button-padding); width: 0;
vertical-align: middle; height: 0;
position: absolute;
border-width: 5px;
border-style: solid;
}
.tooltip.tooltip--top .tooltip__body {
bottom: 100%;
left: 50%;
margin-left: -100px;
&::after {
top: 100%;
left: 50%;
margin-left: -5px;
border-color: var(--tooltip-bg) transparent transparent transparent;
}
}
.tooltip.tooltip--right .tooltip__body {
margin-top: -5px;
margin-left: 10px;
&::after {
top: 17px;
right: 100%; /* To the left of the tooltip */
margin-top: -5px;
border-color: transparent var(--tooltip-bg) transparent transparent;
}
}
.tooltip.tooltip--bottom .tooltip__body {
top: 90%;
left: 50%;
margin-left: -100px;
&::after {
bottom: 100%;
left: 50%;
margin-left: -5px;
border-color: transparent transparent var(--tooltip-bg) transparent;
}
}
.tooltip.tooltip--left .tooltip__body {
top: -5px;
right: 105%;
&::after {
top: 17px;
left: 100%;
margin-top: -5px;
border-color: transparent transparent transparent var(--tooltip-bg);
}
}
.tooltip:hover .tooltip__body {
visibility: visible;
} }

View file

@ -1,28 +0,0 @@
@mixin text-link($color: var(--color-primary), $hover-opacity: 0.7) {
.icon {
&:first-child {
padding-right: 5px;
}
&:last-child:not(:only-child) {
padding-left: 5px;
}
}
&:not(.no-underline) {
text-decoration: underline;
.icon {
text-decoration: none;
}
}
&:hover {
opacity: $hover-opacity;
transition: opacity var(--transition-duration) var(--transition-type);
text-decoration: underline;
.icon {
text-decoration: none;
}
}
color: $color;
cursor: pointer;
}

View file

@ -0,0 +1,68 @@
import { clipboard, remote } from 'electron';
import isDev from 'electron-is-dev';
function injectDevelopmentTemplate(event, templates) {
if (!isDev) return templates;
const { screenX, screenY } = event;
const separator = { type: 'separator' };
const developmentTemplateAddition = [
{
label: 'Inspect element',
accelerator: 'CmdOrCtrl+Shift+I',
click: () => {
remote.getCurrentWindow().inspectElement(screenX, screenY);
},
},
];
if (templates.length > 0) {
templates.push(separator);
}
templates.push(...developmentTemplateAddition);
return templates;
}
export function openContextMenu(event, templates = [], addDefaultTemplates = true) {
if (addDefaultTemplates) {
const { value } = event.target;
const inputTemplates = [
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste',
enabled: clipboard.readText().length > 0,
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall',
enabled: !!value,
},
];
templates.push(...inputTemplates);
}
injectDevelopmentTemplate(event, templates);
remote.Menu.buildFromTemplate(templates).popup();
}
export function openCopyLinkMenu(text, event) {
const templates = [
{
label: 'Copy link',
click: () => {
clipboard.writeText(text);
},
},
];
openContextMenu(event, templates, false);
}
export function initContextMenu(event) {
const { type } = event.target;
if (event.target.matches('input') && (type === 'text' || type === 'number')) {
openContextMenu(event);
} else {
event.preventDefault();
}
}

View file

@ -7,7 +7,7 @@
--color-help: #8696AF; --color-help: #8696AF;
--color-download: rgba(255, 255, 255, 0.75); --color-download: rgba(255, 255, 255, 0.75);
--color-download-overlay: var(--color-black); --color-download-overlay: var(--color-black);
--color-bg: #203049; --color-bg: var(--color-blue-grey);
--color-bg-alt: #2D3D56; --color-bg-alt: #2D3D56;
--color-placeholder: var(--color-bg-alt); --color-placeholder: var(--color-bg-alt);
@ -26,9 +26,11 @@
--input-border-size: 1px; --input-border-size: 1px;
--input-border-color: rgba(255,255,255, 0.5); --input-border-color: rgba(255,255,255, 0.5);
--input-hover-border-color: rgba(255, 255, 255, 1); --input-hover-border-color: rgba(255, 255, 255, 1);
--input-copyable-bg: #203049; --input-copyable-bg: var(--color-blue-grey);
--input-copyable-color: #8696AF; --input-copyable-color: #8696AF;
--input-copyable-border: #53637C; --input-copyable-border: #53637C;
--input-select-bg-color: var(--color-bg-alt);
--input-select-color: var(--color-white);
/* input:disabled */ /* input:disabled */
--input-disabled-border-color: rgba(255, 255, 255, 0.42); --input-disabled-border-color: rgba(255, 255, 255, 0.42);

View file

@ -5837,9 +5837,9 @@ lazy-val@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc" resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc"
lbry-redux@lbryio/lbry-redux#30c18725d8c6c141c30c57f0a324d0abb8963b99: lbry-redux@lbryio/lbry-redux#c41899e78415cae6fcb7bfca0e6ba48bb6bfe6c4:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/30c18725d8c6c141c30c57f0a324d0abb8963b99" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/c41899e78415cae6fcb7bfca0e6ba48bb6bfe6c4"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"