Merge branch 'master' into patch-2

This commit is contained in:
Sean Yesmunt 2018-10-09 16:38:36 -04:00 committed by GitHub
commit b16d5e87de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 622 additions and 990 deletions

View file

@ -40,6 +40,8 @@
"react/require-default-props": 0,
"react/jsx-closing-tag-location": 0,
"jsx-a11y/no-noninteractive-element-to-interactive-role": 0,
"class-methods-use-this": 0
"class-methods-use-this": 0,
"jsx-a11y/interactive-supports-focus": 0,
"jsx-a11y/click-events-have-key-events": 0
}
}

View file

@ -6,14 +6,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
## [Unreleased]
### Added
* Allow typing of encryption password without clicking entry box ([#1977](https://github.com/lbryio/lbry-desktop/pull/1977))
* Allow typing of encryption password without clicking entry box ([#1977](https://github.com/lbryio/lbry-desktop/pull/1977))
* Focus on search bar with {cmd,ctrl} + "l" ([#2003](https://github.com/lbryio/lbry-desktop/pull/2003))
### Changed
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979))
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979))
* Change channel pages to have 48 items instead of 10 ([#2002](https://github.com/lbryio/lbry-desktop/pull/2002))
* Update to https ([#2016](https://github.com/lbryio/lbry-desktop/pull/2016))
* Simplify FileCard and FileTile component styling ([#2011](https://github.com/lbryio/lbry-desktop/pull/2011))
### Fixed
* Invite table cutoff with large number of invites ([#1985](https://github.com/lbryio/lbry-desktop/pull/1985))
* Fixed Transactions filter menu collides with transaction table ([#2005](https://github.com/lbryio/lbry-desktop/pull/2005))
* Fixed Transactions filter menu collides with transaction table ([#2005](https://github.com/lbryio/lbry-desktop/pull/2005))
* Invite table cutoff with large number of invites ([#1985](https://github.com/lbryio/lbry-desktop/pull/1985))
* History styling on large screens and link issue with claims ([#1999](https://github.com/lbryio/lbry-desktop/pull/1999))
* Satisfy console warnings in publishForm and validation messaging ([#2010](https://github.com/lbryio/lbry-desktop/pull/2010))
## [0.25.1] - 2018-09-18

View file

@ -23,12 +23,10 @@
"extract-langs": "node build/extractLocals.js",
"compile": "electron-webpack && yarn extract-langs",
"build": "yarn compile && electron-builder build",
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
"dev": "electron-webpack dev",
"lint": "eslint 'src/**/*.{js,jsx}' --fix && flow",
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
"flow-defs": "flow-typed install",
"release": "yarn compile && electron-builder build",
"precommit": "lint-staged",
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js"
@ -50,7 +48,7 @@
"formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#c079b108c3bc4ba2b4fb85fb112b52cfc040c301",
"lbry-redux": "lbryio/lbry-redux#4ee6c376e5f2c3e3e96d199a56970e2621a84af1",
"lbryinc": "lbryio/lbryinc#de7ff055605b02a24821f0f9bab1d206eb7f235d",
"localforage": "^1.7.1",
"mammoth": "^1.4.6",

View file

@ -1,5 +1,5 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'component/button';
import * as icons from 'constants/icons';
@ -7,30 +7,29 @@ let scriptLoading = false;
let scriptLoaded = false;
let scriptDidError = false;
type Props = {
disabled: boolean,
label: ?string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: string,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: string,
};
class CardVerify extends React.Component {
static propTypes = {
disabled: PropTypes.bool,
label: PropTypes.string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: PropTypes.string.isRequired,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {

View file

@ -60,32 +60,13 @@ class ChannelTile extends React.PureComponent<Props> {
>
<CardMedia title={channelName} thumbnail={null} />
<div className="file-tile__info">
{isResolvingUri && (
<div
className={classnames({
'card__title--small': size !== 'large',
'card__title--large': size === 'large',
})}
>
{__('Loading...')}
</div>
)}
{isResolvingUri && <div className="file-tile__title">{__('Loading...')}</div>}
{!isResolvingUri && (
<React.Fragment>
<div
className={classnames({
'card__title--file': size === 'regular',
'card__title--x-small': size === 'small',
'card__title--large': size === 'large',
})}
>
<div className="file-tile__title">
<TruncatedText text={channelName || uri} lines={1} />
</div>
<div
className={classnames('card__subtitle', {
'card__subtitle--large': size === 'large',
})}
>
<div className="card__subtitle">
{totalItems > 0 && (
<span>
{totalItems} {totalItems === 1 ? 'file' : 'files'}

View file

@ -14,6 +14,7 @@ type Props = {
icon: string,
tooltip?: string, // tooltip direction
iconColor?: string,
size?: number,
};
class IconComponent extends React.PureComponent<Props> {
@ -42,7 +43,7 @@ class IconComponent extends React.PureComponent<Props> {
};
render() {
const { icon, tooltip, iconColor } = this.props;
const { icon, tooltip, iconColor, size } = this.props;
const Icon = FeatherIcons[icon];
if (!Icon) {
@ -54,16 +55,17 @@ class IconComponent extends React.PureComponent<Props> {
color = this.getIconColor(iconColor);
}
let size = 14;
let iconSize = size || 14;
// Arrow icons are quite a bit smaller than the other icons we use
if (icon === icons.ARROW_LEFT || icon === icons.ARROW_RIGHT) {
size = 20;
iconSize = 20;
}
let tooltipText;
if (tooltip) {
tooltipText = this.getTooltip(icon);
}
const inner = <Icon size={size} className="icon" color={color} />;
const inner = <Icon size={iconSize} className="icon" color={color} />;
return tooltipText ? (
<Tooltip icon body={tooltipText} direction={tooltip}>

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { doNotify } from 'lbry-redux';
import CopyableText from './view';
export default connect(
null,
{
doNotify,
}
)(CopyableText);

View file

@ -0,0 +1,63 @@
// @flow
import * as React from 'react';
import { clipboard } from 'electron';
import { FormRow } from 'component/common/form';
import Button from 'component/button';
import * as icons from 'constants/icons';
/*
noSnackbar added due to issue 1945
https://github.com/lbryio/lbry-desktop/issues/1945
"Snackbars and modals can't be displayed at the same time"
*/
type Props = {
copyable: string,
noSnackbar: boolean,
doNotify: ({ message: string, displayType: Array<string> }) => void,
};
export default class CopyableText extends React.PureComponent<Props> {
constructor() {
super();
this.input = null;
}
input: ?HTMLInputElement;
render() {
const { copyable, doNotify, noSnackbar } = this.props;
return (
<FormRow verticallyCentered padded stretch>
<input
className="input-copyable form-field__input"
readOnly
value={copyable || ''}
ref={input => {
this.input = input;
}}
onFocus={() => {
if (this.input) {
this.input.select();
}
}}
/>
<Button
noPadding
button="secondary"
icon={icons.CLIPBOARD}
onClick={() => {
clipboard.writeText(copyable);
if (!noSnackbar) {
doNotify({
message: __('Text copied'),
displayType: ['snackbar'],
});
}
}}
/>
</FormRow>
);
}
}

View file

@ -91,18 +91,16 @@ class FileCard extends React.PureComponent<Props> {
onContextMenu={handleContextMenu}
>
<CardMedia thumbnail={thumbnail} />
<div className="card__title-identity">
<div className="card__title--small card__title--file-card">
<TruncatedText text={title} lines={2} />
</div>
<div className="card__subtitle">
{pending ? <div>Pending...</div> : <UriIndicator uri={uri} link />}
</div>
<div className="card__file-properties">
{showPrice && <FilePrice hideFree uri={uri} />}
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
</div>
<div className="card__title card__title--file-card">
<TruncatedText text={title} lines={2} />
</div>
<div className="card__subtitle">
{pending ? <div>Pending...</div> : <UriIndicator uri={uri} link />}
</div>
<div className="card__file-properties">
{showPrice && <FilePrice hideFree uri={uri} />}
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
</div>
</section>
);

View file

@ -108,36 +108,14 @@ class FileTile extends React.PureComponent<Props> {
>
<CardMedia title={title || name} thumbnail={thumbnail} />
<div className="file-tile__info">
{isResolvingUri && (
<div
className={classnames({
'card__title--small': size !== 'large',
'card__title--large': size === 'large',
})}
>
{__('Loading...')}
</div>
)}
{isResolvingUri && <div className="file-tile__title">{__('Loading...')}</div>}
{!isResolvingUri && (
<React.Fragment>
<div
className={classnames({
'card__title--file': size === 'regular',
'card__title--x-small': size === 'small',
'card__title--large': size === 'large',
})}
>
<div className="file-tile__title">
<TruncatedText text={title || name} lines={size === 'small' ? 2 : 3} />
</div>
<div
className={classnames('card__subtitle', {
'card__subtitle--x-small': size === 'small',
'card__subtitle--large': size === 'large',
})}
>
<span className="file-tile__channel">
{showUri ? uri : <UriIndicator uri={uri} link />}
</span>
<div className="card__subtitle">
{showUri ? uri : <UriIndicator uri={uri} link />}
</div>
<div className="card__file-properties">
<FilePrice hideFree uri={uri} />
@ -145,12 +123,7 @@ class FileTile extends React.PureComponent<Props> {
{showLocal && isDownloaded && <Icon icon={icons.LOCAL} />}
</div>
{displayDescription && (
<div
className={classnames('card__subtext', {
'card__subtext--small': size !== 'small',
'card__subtext--large': size === 'large',
})}
>
<div className="card__subtext">
<TruncatedText text={description} lines={size === 'large' ? 4 : 3} />
</div>
)}

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import FormField from './view';
export default connect(null, null, null, { withRef: true })(FormField);

View file

@ -1,200 +0,0 @@
// This file is going to die
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import FileSelector from 'component/common/file-selector';
import SimpleMDE from 'react-simplemde-editor';
import { formFieldNestedLabelTypes, formFieldId } from 'component/common/form';
import style from 'react-simplemde-editor/dist/simplemde.min.css';
const formFieldFileSelectorTypes = ['file', 'directory'];
class FormField extends React.PureComponent {
static propTypes = {
type: PropTypes.string.isRequired,
prefix: PropTypes.string,
postfix: PropTypes.string,
hasError: PropTypes.bool,
trim: PropTypes.bool,
regexp: PropTypes.oneOfType([PropTypes.instanceOf(RegExp), PropTypes.string]),
};
static defaultProps = {
trim: false,
};
constructor(props) {
super(props);
this._fieldRequiredText = __('This field is required');
this._type = null;
this._element = null;
this._extraElementProps = {};
this.state = {
isError: null,
errorMessage: null,
};
}
componentWillMount() {
if (['text', 'number', 'radio', 'checkbox'].includes(this.props.type)) {
this._element = 'input';
this._type = this.props.type;
} else if (this.props.type == 'text-number') {
this._element = 'input';
this._type = 'text';
} else if (this.props.type == 'SimpleMDE') {
this._element = SimpleMDE;
this._type = 'textarea';
this._extraElementProps.options = {
placeholder: this.props.placeholder,
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
};
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = 'input';
this._type = 'hidden';
} else {
// Non <input> field, e.g. <select>, <textarea>
this._element = this.props.type;
}
}
componentDidMount() {
/**
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
* https://github.com/facebook/react/issues/3468
*/
if (this.props.type == 'directory') {
this.refs.field.webkitdirectory = true;
}
}
handleFileChosen(path) {
this.refs.field.value = path;
if (this.props.onChange) {
// Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event('change', { bubbles: true });
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
this.props.onChange(event);
}
}
showError(text) {
this.setState({
isError: true,
errorMessage: text,
});
}
clearError() {
this.setState({
isError: false,
errorMessage: '',
});
}
getValue() {
if (this.props.type == 'checkbox') {
return this.refs.field.checked;
} else if (this.props.type == 'SimpleMDE') {
return this.refs.field.simplemde.value();
}
return this.props.trim ? this.refs.field.value.trim() : this.refs.field.value;
}
getSelectedElement() {
return this.refs.field.options[this.refs.field.selectedIndex];
}
getOptions() {
return this.refs.field.options;
}
validate() {
if ('regexp' in this.props) {
if (!this.getValue().match(this.props.regexp)) {
this.showError(__('Invalid format.'));
} else {
this.clearError();
}
}
this.props.onBlur && this.props.onBlur();
}
focus() {
this.refs.field.focus();
}
render() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
elementId = this.props.elementId ? this.props.elementId : formFieldId(),
renderElementInsideLabel =
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
delete otherProps.type;
delete otherProps.label;
delete otherProps.hasError;
delete otherProps.className;
delete otherProps.postfix;
delete otherProps.prefix;
delete otherProps.dispatch;
delete otherProps.regexp;
delete otherProps.trim;
const element = (
<this._element
id={elementId}
type={this._type}
name={this.props.name}
ref="field"
placeholder={this.props.placeholder}
onBlur={() => this.validate()}
onFocus={() => this.props.onFocus && this.props.onFocus()}
className={`form-field__input form-field__input-${this.props.type} ${this.props.className ||
''}${isError ? 'form-field__input--error' : ''}`}
{...otherProps}
{...this._extraElementProps}
>
{this.props.children}
</this._element>
);
return (
<div className={`form-field form-field--${this.props.type}`}>
{this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : ''}
{element}
{renderElementInsideLabel && (
<label
htmlFor={elementId}
className={`form-field__label ${isError ? 'form-field__label--error' : ''}`}
>
{this.props.label}
</label>
)}
{formFieldFileSelectorTypes.includes(this.props.type) ? (
<FileSelector
type={this.props.type}
onFileChosen={this.handleFileChosen.bind(this)}
{...(this.props.defaultValue ? { initPath: this.props.defaultValue } : {})}
/>
) : null}
{this.props.postfix ? (
<span className="form-field__postfix">{this.props.postfix}</span>
) : (
''
)}
{isError && this.state.errorMessage ? (
<div className="form-field__error">{this.state.errorMessage}</div>
) : (
''
)}
</div>
);
}
}
export default FormField;
/* eslint-enable */

View file

@ -5,13 +5,11 @@ import { CC_LICENSES, COPYRIGHT, OTHER, PUBLIC_DOMAIN, NONE } from 'constants/li
type Props = {
licenseType: string,
copyrightNotice: ?string,
licenseUrl: ?string,
otherLicenseDescription: ?string,
handleLicenseChange: (string, string) => void,
handleLicenseDescriptionChange: (SyntheticInputEvent<*>) => void,
handleLicenseUrlChange: (SyntheticInputEvent<*>) => void,
handleCopyrightNoticeChange: (SyntheticInputEvent<*>) => void,
};
class LicenseType extends React.PureComponent<Props> {
@ -38,11 +36,8 @@ class LicenseType extends React.PureComponent<Props> {
licenseType,
otherLicenseDescription,
licenseUrl,
copyrightNotice,
handleLicenseChange,
handleLicenseDescriptionChange,
handleLicenseUrlChange,
handleCopyrightNoticeChange,
} = this.props;
return (
@ -72,8 +67,8 @@ class LicenseType extends React.PureComponent<Props> {
label={__('Copyright notice')}
type="text"
name="copyright-notice"
value={copyrightNotice}
onChange={handleCopyrightNoticeChange}
value={otherLicenseDescription}
onChange={handleLicenseDescriptionChange}
/>
</FormRow>
)}

View file

@ -37,7 +37,6 @@ type Props = {
name: ?string,
tosAccepted: boolean,
updatePublishForm: UpdatePublishFormData => void,
bid: number,
nameError: ?string,
isResolvingUri: boolean,
winningBidForClaimUri: number,
@ -45,7 +44,6 @@ type Props = {
licenseType: string,
otherLicenseDescription: ?string,
licenseUrl: ?string,
copyrightNotice: ?string,
uri: ?string,
bidError: ?string,
publishing: boolean,
@ -200,7 +198,6 @@ class PublishForm extends React.PureComponent<Props> {
handlePublish() {
const {
filePath,
copyrightNotice,
licenseType,
licenseUrl,
otherLicenseDescription,
@ -211,8 +208,6 @@ class PublishForm extends React.PureComponent<Props> {
let publishingLicense;
switch (licenseType) {
case COPYRIGHT:
publishingLicense = copyrightNotice;
break;
case OTHER:
publishingLicense = otherLicenseDescription;
break;
@ -233,7 +228,6 @@ class PublishForm extends React.PureComponent<Props> {
license: publishingLicense,
licenseUrl: publishingLicenseUrl,
otherLicenseDescription,
copyrightNotice,
name: this.props.name,
contentIsFree: this.props.contentIsFree,
price: this.props.price,
@ -301,7 +295,7 @@ class PublishForm extends React.PureComponent<Props> {
{!title && <div>{__('A title is required')}</div>}
{!name && <div>{__('A URL is required')}</div>}
{name && nameError && <div>{__('The URL you created is not valid')}</div>}
{!bid && <div>{__('A bid amount is required')}</div>}
{!bid && <div>{__('A deposit amount is required')}</div>}
{!!bid && bidError && <div>{bidError}</div>}
{uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS && (
<div>{__('Please wait for thumbnail to finish uploading')}</div>
@ -339,7 +333,6 @@ class PublishForm extends React.PureComponent<Props> {
licenseType,
otherLicenseDescription,
licenseUrl,
copyrightNotice,
uri,
bidError,
publishing,
@ -535,7 +528,7 @@ class PublishForm extends React.PureComponent<Props> {
step="any"
label={__('Deposit')}
postfix="LBC"
value={bid}
value={bid || ''}
error={bidError}
min="0"
disabled={!name}
@ -585,7 +578,6 @@ class PublishForm extends React.PureComponent<Props> {
licenseType={licenseType}
otherLicenseDescription={otherLicenseDescription}
licenseUrl={licenseUrl}
copyrightNotice={copyrightNotice}
handleLicenseChange={(newLicenseType, newLicenseUrl) =>
updatePublishForm({
licenseType: newLicenseType,
@ -600,9 +592,6 @@ class PublishForm extends React.PureComponent<Props> {
handleLicenseUrlChange={event =>
updatePublishForm({ licenseUrl: event.target.value })
}
handleCopyrightNoticeChange={event =>
updatePublishForm({ copyrightNotice: event.target.value })
}
/>
</section>

View file

@ -1,6 +1,10 @@
import { connect } from 'react-redux';
import { selectUnclaimedRewardValue, selectFetchingRewards, doRewardList } from 'lbryinc';
import { doFetchRewardedContent } from 'redux/actions/content';
import {
selectUnclaimedRewardValue,
selectFetchingRewards,
doRewardList,
doFetchRewardedContent,
} from 'lbryinc';
import RewardSummary from './view';
const select = state => ({

View file

@ -89,7 +89,7 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
type="text"
name="content_thumbnail"
label="URL"
placeholder="http://spee.ch/mylogo"
placeholder="https://spee.ch/mylogo"
value={thumbnail}
disabled={formDisabled}
onChange={this.handleThumbnailChange}

View file

@ -101,7 +101,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
)}
{shiftState === statuses.RECEIVED && (
<div className="card__content--extra-vertical-space">
<div>
<p>
{__('ShapeShift has received your payment! Sending the funds to your LBRY wallet.')}
</p>
@ -110,7 +110,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
)}
{shiftState === statuses.COMPLETE && (
<div className="card__content--extra-vertical-space">
<div>
<p>{__('Transaction complete! You should see the new LBC in your wallet.')}</p>
</div>
)}

View file

@ -3,12 +3,13 @@ import React from 'react';
import type { Claim } from 'types/claim';
import Button from 'component/button';
import * as icons from 'constants/icons';
import Tooltip from 'component/common/tooltip';
import Address from 'component/address';
import CopyableText from 'component/copyableText';
import ToolTip from 'component/common/tooltip';
type Props = {
claim: Claim,
onDone: () => void,
speechShareable: boolean,
};
class SocialShare extends React.PureComponent<Props> {
@ -27,46 +28,79 @@ class SocialShare extends React.PureComponent<Props> {
channel_name: channelName,
value,
} = this.props.claim;
const { speechShareable, onDone } = this.props;
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
const { onDone } = this.props;
const speechPrefix = 'http://spee.ch/';
const speechPrefix = 'https://spee.ch/';
const lbryPrefix = 'https://open.lbry.io/';
const speechURL =
channelName && channelClaimId
? `${speechPrefix}${channelName}:${channelClaimId}/${claimName}`
: `${speechPrefix}${claimName}#${claimId}`;
const lbryURL = `${lbryPrefix}${claimName}#${claimId}`;
return (
<section className="card__content">
<Address address={speechURL} noSnackbar />
<div className="card__actions card__actions--center">
<Tooltip onComponent body={__('Facebook')}>
<Button
iconColor="blue"
icon={icons.FACEBOOK}
button="alt"
label={__('')}
href={`https://facebook.com/sharer/sharer.php?u=${speechURL}`}
/>
</Tooltip>
<Tooltip onComponent body={__('Twitter')}>
<Button
iconColor="blue"
icon={icons.TWITTER}
button="alt"
label={__('')}
href={`https://twitter.com/home?status=${speechURL}`}
/>
</Tooltip>
<Tooltip onComponent body={__('View on Spee.ch')}>
<Button
icon={icons.GLOBE}
iconColor="blue"
button="alt"
label={__('')}
href={`${speechURL}`}
/>
</Tooltip>
{speechShareable && (
<div className="card__content">
<label className="card__subtitle">{__('Web link')}</label>
<CopyableText copyable={speechURL} noSnackbar />
<div className="card__actions card__actions--center">
<ToolTip onComponent body={__('Facebook')}>
<Button
iconColor="blue"
icon={icons.FACEBOOK}
button="alt"
label={__('')}
href={`https://facebook.com/sharer/sharer.php?u=${speechURL}`}
/>
</ToolTip>
<ToolTip onComponent body={__('Twitter')}>
<Button
iconColor="blue"
icon={icons.TWITTER}
button="alt"
label={__('')}
href={`https://twitter.com/home?status=${speechURL}`}
/>
</ToolTip>
<ToolTip onComponent body={__('View on Spee.ch')}>
<Button
icon={icons.GLOBE}
iconColor="blue"
button="alt"
label={__('')}
href={`${speechURL}`}
/>
</ToolTip>
</div>
</div>
)}
<div className="card__content">
<label className="card__subtitle">{__('LBRY App link')}</label>
<CopyableText copyable={lbryURL} noSnackbar />
<div className="card__actions card__actions--center">
<ToolTip onComponent body={__('Facebook')}>
<Button
iconColor="blue"
icon={icons.FACEBOOK}
button="alt"
label={__('')}
href={`https://facebook.com/sharer/sharer.php?u=${lbryURL}`}
/>
</ToolTip>
<ToolTip onComponent body={__('Twitter')}>
<Button
iconColor="blue"
icon={icons.TWITTER}
button="alt"
label={__('')}
href={`https://twitter.com/home?status=${lbryURL}`}
/>
</ToolTip>
</div>
</div>
<div className="card__actions">
<Button button="link" label={__('Done')} onClick={onDone} />

View file

@ -24,6 +24,10 @@ class TransactionListRecent extends React.PureComponent<Props> {
return (
<section className="card card--section">
<div className="card__title">{__('Recent Transactions')}</div>
<div className="card__subtitle">
{__('To view all of your transactions, navigate to the')}{' '}
<Button button="link" navigate="/history" label={__('transactions page')} />.
</div>
{fetchingTransactions && (
<div className="card__content">
<BusyIndicator message={__('Loading transactions')} />

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import TruncatedMarkdown from './view';
export default connect()(TruncatedMarkdown);

View file

@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactMarkdown from 'react-markdown';
import ReactDOMServer from 'react-dom/server';
class TruncatedMarkdown extends React.PureComponent {
static propTypes = {
lines: PropTypes.number,
};
static defaultProps = {
lines: null,
};
transformMarkdown(text) {
// render markdown to html string then trim html tag
const htmlString = ReactDOMServer.renderToStaticMarkup(
<ReactMarkdown source={this.props.children} />
);
const txt = document.createElement('textarea');
txt.innerHTML = htmlString;
return txt.value.replace(/<(?:.|\n)*?>/gm, '');
}
render() {
const content =
this.props.children && typeof this.props.children === 'string'
? this.transformMarkdown(this.props.children)
: this.props.children;
return (
<span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>
{content}
</span>
);
}
}
export default TruncatedMarkdown;

View file

@ -2,7 +2,6 @@
import React from 'react';
import Button from 'component/button';
import { buildURI } from 'lbry-redux';
import classnames from 'classnames';
import type { Claim } from 'types/claim';
type Props = {
@ -59,17 +58,7 @@ class UriIndicator extends React.PureComponent<Props> {
channelLink = link ? buildURI({ channelName, claimId: channelClaimId }) : false;
}
const inner = (
<span>
<span
className={classnames('channel-name', {
'button-text no-underline': link,
})}
>
{channelName}
</span>{' '}
</span>
);
const inner = <span className="channel-name">{channelName}</span>;
if (!channelLink) {
return inner;

View file

@ -99,7 +99,8 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
const allSelected = Object.keys(itemsSelected).length === history.length;
const selectHandler = allSelected ? this.unselectAll : this.selectAll;
return (
return history.length ? (
<React.Fragment>
<div className="card__actions card__actions--between">
{Object.keys(itemsSelected).length ? (
@ -109,7 +110,6 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
{/* Using an empty span so spacing stays the same if the button isn't rendered */}
</span>
)}
<Button
button="link"
label={allSelected ? __('Cancel') : __('Select All')}
@ -117,21 +117,19 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
/>
</div>
{!!history.length && (
<table className="card--section table table--stretch table--history">
<tbody>
{history.map(item => (
<UserHistoryItem
key={item.uri}
uri={item.uri}
lastViewed={item.lastViewed}
selected={!!itemsSelected[item.uri]}
onSelect={() => {
this.onSelect(item.uri);
}}
/>
))}
</tbody>
</table>
<section className="item-list">
{history.map(item => (
<UserHistoryItem
key={item.uri}
uri={item.uri}
lastViewed={item.lastViewed}
selected={!!itemsSelected[item.uri]}
onSelect={() => {
this.onSelect(item.uri);
}}
/>
))}
</section>
)}
{pageCount > 1 && (
<FormRow padded verticallyCentered centered>
@ -161,6 +159,13 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
</FormRow>
)}
</React.Fragment>
) : (
<div className="page__empty">
{__("You don't have anything saved in history yet, go check out some content on LBRY!")}
<div className="card__actions card__actions--center">
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div>
</div>
);
}
}

View file

@ -36,27 +36,24 @@ class UserHistoryItem extends React.PureComponent<Props> {
}
return (
<tr
<div
role="button"
onClick={onSelect}
className={classnames({
history__selected: selected,
className={classnames('item-list__item', {
'item-list__item--selected': selected,
})}
>
<td>
<input checked={selected} type="checkbox" onClick={onSelect} />
</td>
<td>{moment(lastViewed).from(moment())}</td>
<td>{title}</td>
<td>
<Button
tourniquet
button="link"
label={name ? `lbry://${name}` : `lbry://...`}
navigate="/show"
navigateParams={{ uri }}
/>
</td>
</tr>
<input checked={selected} type="checkbox" onClick={onSelect} />
<span className="time time--ago">{moment(lastViewed).from(moment())}</span>
<span className="item-list__item--cutoff">{title}</span>
<Button
tourniquet
button="link"
label={name ? `lbry://${name}` : `lbry://...`}
navigate="/show"
navigateParams={{ uri }}
/>
</div>
);
}
}

View file

@ -2,7 +2,7 @@
import * as React from 'react';
import Button from 'component/button';
import CardVerify from 'component/cardVerify';
import Lbryio from 'lbryinc';
import { Lbryio } from 'lbryinc';
import * as icons from 'constants/icons';
type Props = {

View file

@ -19,7 +19,7 @@ export default (props: Props) => {
icon={icons.GLOBE}
button="alt"
label={__('Share')}
href={`http://spee.ch/${speechURL}`}
href={`https://spee.ch/${speechURL}`}
/>
) : null;
};

View file

@ -16,7 +16,6 @@ https://github.com/reactjs/react-autocomplete/issues/239
/* eslint-disable */
const React = require('react');
const PropTypes = require('prop-types');
const { findDOMNode } = require('react-dom');
const scrollIntoView = require('dom-scroll-into-view');
@ -45,129 +44,129 @@ function getScrollOffset() {
}
export default class Autocomplete extends React.Component {
static propTypes = {
/**
* The items to display in the dropdown menu
*/
items: PropTypes.array.isRequired,
/**
* The value to display in the input field
*/
value: PropTypes.any,
/**
* Arguments: `event: Event, value: String`
*
* Invoked every time the user changes the input's value.
*/
onChange: PropTypes.func,
/**
* Arguments: `value: String, item: Any`
*
* Invoked when the user selects an item from the dropdown menu.
*/
onSelect: PropTypes.func,
/**
* Arguments: `item: Any, value: String`
*
* Invoked for each entry in `items` and its return value is used to
* determine whether or not it should be displayed in the dropdown menu.
* By default all items are always rendered.
*/
shouldItemRender: PropTypes.func,
/**
* Arguments: `itemA: Any, itemB: Any, value: String`
*
* The function which is used to sort `items` before display.
*/
sortItems: PropTypes.func,
/**
* Arguments: `item: Any`
*
* Used to read the display value from each entry in `items`.
*/
getItemValue: PropTypes.func.isRequired,
/**
* Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
*
* Invoked for each entry in `items` that also passes `shouldItemRender` to
* generate the render tree for each item in the dropdown menu. `styles` is
* an optional set of styles that can be applied to improve the look/feel
* of the items in the dropdown menu.
*/
renderItem: PropTypes.func.isRequired,
/**
* Arguments: `items: Array<Any>, value: String, styles: Object`
*
* Invoked to generate the render tree for the dropdown menu. Ensure the
* returned tree includes every entry in `items` or else the highlight order
* and keyboard navigation logic will break. `styles` will contain
* { top, left, minWidth } which are the coordinates of the top-left corner
* and the width of the dropdown menu.
*/
renderMenu: PropTypes.func,
/**
* Styles that are applied to the dropdown menu in the default `renderMenu`
* implementation. If you override `renderMenu` and you want to use
* `menuStyle` you must manually apply them (`this.props.menuStyle`).
*/
menuStyle: PropTypes.object,
/**
* Arguments: `props: Object`
*
* Invoked to generate the input element. The `props` argument is the result
* of merging `props.inputProps` with a selection of props that are required
* both for functionality and accessibility. At the very least you need to
* apply `props.ref` and all `props.on<event>` event handlers. Failing to do
* this will cause `Autocomplete` to behave unexpectedly.
*/
renderInput: PropTypes.func,
/**
* Props passed to `props.renderInput`. By default these props will be
* applied to the `<input />` element rendered by `Autocomplete`, unless you
* have specified a custom value for `props.renderInput`. Any properties
* supported by `HTMLInputElement` can be specified, apart from the
* following which are set by `Autocomplete`: value, autoComplete, role,
* aria-autocomplete. `inputProps` is commonly used for (but not limited to)
* placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
*/
inputProps: PropTypes.object,
/**
* Props that are applied to the element which wraps the `<input />` and
* dropdown menu elements rendered by `Autocomplete`.
*/
wrapperProps: PropTypes.object,
/**
* This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
* Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
* will win if it contains a `style` entry.
*/
wrapperStyle: PropTypes.object,
/**
* Whether or not to automatically highlight the top match in the dropdown
* menu.
*/
autoHighlight: PropTypes.bool,
/**
* Whether or not to automatically select the highlighted item when the
* `<input>` loses focus.
*/
selectOnBlur: PropTypes.bool,
/**
* Arguments: `isOpen: Boolean`
*
* Invoked every time the dropdown menu's visibility changes (i.e. every
* time it is displayed/hidden).
*/
onMenuVisibilityChange: PropTypes.func,
/**
* Used to override the internal logic which displays/hides the dropdown
* menu. This is useful if you want to force a certain state based on your
* UX/business logic. Use it together with `onMenuVisibilityChange` for
* fine-grained control over the dropdown menu dynamics.
*/
open: PropTypes.bool,
debug: PropTypes.bool,
};
// static propTypes = {
// /**
// * The items to display in the dropdown menu
// */
// items: PropTypes.array.isRequired,
// /**
// * The value to display in the input field
// */
// value: PropTypes.any,
// /**
// * Arguments: `event: Event, value: String`
// *
// * Invoked every time the user changes the input's value.
// */
// onChange: PropTypes.func,
// /**
// * Arguments: `value: String, item: Any`
// *
// * Invoked when the user selects an item from the dropdown menu.
// */
// onSelect: PropTypes.func,
// /**
// * Arguments: `item: Any, value: String`
// *
// * Invoked for each entry in `items` and its return value is used to
// * determine whether or not it should be displayed in the dropdown menu.
// * By default all items are always rendered.
// */
// shouldItemRender: PropTypes.func,
// /**
// * Arguments: `itemA: Any, itemB: Any, value: String`
// *
// * The function which is used to sort `items` before display.
// */
// sortItems: PropTypes.func,
// /**
// * Arguments: `item: Any`
// *
// * Used to read the display value from each entry in `items`.
// */
// getItemValue: PropTypes.func.isRequired,
// /**
// * Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
// *
// * Invoked for each entry in `items` that also passes `shouldItemRender` to
// * generate the render tree for each item in the dropdown menu. `styles` is
// * an optional set of styles that can be applied to improve the look/feel
// * of the items in the dropdown menu.
// */
// renderItem: PropTypes.func.isRequired,
// /**
// * Arguments: `items: Array<Any>, value: String, styles: Object`
// *
// * Invoked to generate the render tree for the dropdown menu. Ensure the
// * returned tree includes every entry in `items` or else the highlight order
// * and keyboard navigation logic will break. `styles` will contain
// * { top, left, minWidth } which are the coordinates of the top-left corner
// * and the width of the dropdown menu.
// */
// renderMenu: PropTypes.func,
// /**
// * Styles that are applied to the dropdown menu in the default `renderMenu`
// * implementation. If you override `renderMenu` and you want to use
// * `menuStyle` you must manually apply them (`this.props.menuStyle`).
// */
// menuStyle: PropTypes.object,
// /**
// * Arguments: `props: Object`
// *
// * Invoked to generate the input element. The `props` argument is the result
// * of merging `props.inputProps` with a selection of props that are required
// * both for functionality and accessibility. At the very least you need to
// * apply `props.ref` and all `props.on<event>` event handlers. Failing to do
// * this will cause `Autocomplete` to behave unexpectedly.
// */
// renderInput: PropTypes.func,
// /**
// * Props passed to `props.renderInput`. By default these props will be
// * applied to the `<input />` element rendered by `Autocomplete`, unless you
// * have specified a custom value for `props.renderInput`. Any properties
// * supported by `HTMLInputElement` can be specified, apart from the
// * following which are set by `Autocomplete`: value, autoComplete, role,
// * aria-autocomplete. `inputProps` is commonly used for (but not limited to)
// * placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
// */
// inputProps: PropTypes.object,
// /**
// * Props that are applied to the element which wraps the `<input />` and
// * dropdown menu elements rendered by `Autocomplete`.
// */
// wrapperProps: PropTypes.object,
// /**
// * This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
// * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
// * will win if it contains a `style` entry.
// */
// wrapperStyle: PropTypes.object,
// /**
// * Whether or not to automatically highlight the top match in the dropdown
// * menu.
// */
// autoHighlight: PropTypes.bool,
// /**
// * Whether or not to automatically select the highlighted item when the
// * `<input>` loses focus.
// */
// selectOnBlur: PropTypes.bool,
// /**
// * Arguments: `isOpen: Boolean`
// *
// * Invoked every time the dropdown menu's visibility changes (i.e. every
// * time it is displayed/hidden).
// */
// onMenuVisibilityChange: PropTypes.func,
// /**
// * Used to override the internal logic which displays/hides the dropdown
// * menu. This is useful if you want to force a certain state based on your
// * UX/business logic. Use it together with `onMenuVisibilityChange` for
// * fine-grained control over the dropdown menu dynamics.
// */
// open: PropTypes.bool,
// debug: PropTypes.bool,
// };
static defaultProps = {
value: '',

View file

@ -7,24 +7,36 @@ import { parseQueryParams } from 'util/query_params';
import * as icons from 'constants/icons';
import Autocomplete from './internal/autocomplete';
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
type Props = {
updateSearchQuery: string => void,
onSearch: string => void,
onSearch: (string, ?number) => void,
onSubmit: (string, {}) => void,
wunderbarValue: ?string,
suggestions: Array<string>,
doFocus: () => void,
doBlur: () => void,
resultCount: number,
focused: boolean,
};
class WunderBar extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
constructor() {
super();
(this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleChange = this.handleChange.bind(this);
this.input = undefined;
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
}
getSuggestionIcon = (type: string) => {
@ -38,6 +50,31 @@ class WunderBar extends React.PureComponent<Props> {
}
};
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
const { ctrlKey, metaKey, keyCode } = event;
const { doFocus, doBlur, focused } = this.props;
if (!this.input) {
return;
}
if (focused && keyCode === ESC_KEY_CODE) {
doBlur();
this.input.blur();
return;
}
const shouldFocus =
process.platform === 'darwin'
? keyCode === L_KEY_CODE && metaKey
: keyCode === L_KEY_CODE && ctrlKey;
if (shouldFocus) {
doFocus();
this.input.focus();
}
}
handleChange(e: SyntheticInputEvent<*>) {
const { updateSearchQuery } = this.props;
const { value } = e.target;
@ -106,6 +143,10 @@ class WunderBar extends React.PureComponent<Props> {
renderInput={props => (
<input
{...props}
ref={el => {
props.ref(el);
this.input = el;
}}
className="wunderbar__input"
placeholder="Enter LBRY URL here or search for videos, music, games and more"
/>

View file

@ -6,14 +6,15 @@ import SocialShare from 'component/socialShare';
type Props = {
closeModal: () => void,
uri: string,
speechShareable: boolean,
};
class ModalSocialShare extends React.PureComponent<Props> {
render() {
const { closeModal, uri } = this.props;
const { closeModal, uri, speechShareable } = this.props;
return (
<Modal isOpen onAborted={closeModal} type="custom" title={__('Share')}>
<SocialShare uri={uri} onDone={closeModal} />
<SocialShare uri={uri} onDone={closeModal} speechShareable={speechShareable} />
</Modal>
);
}

View file

@ -80,17 +80,17 @@ class ChannelPage extends React.PureComponent<Props> {
return (
<Page notContained>
<section className="card__channel-info card__channel-info--large">
<section>
<h1>
{name}
{fetching && <BusyIndicator />}
</h1>
<div className="card__actions card__actions--no-margin">
<SubscribeButton uri={permanentUrl} channelName={name} />
<ViewOnWebButton claimId={claimId} claimName={name} />
</div>
</section>
<section>{contentList}</section>
<div className="card__actions">
<SubscribeButton uri={permanentUrl} channelName={name} />
<ViewOnWebButton claimId={claimId} claimName={name} />
</div>
<section className="card__content">{contentList}</section>
{(!fetching || (claimsInChannel && claimsInChannel.length)) &&
totalPages > 1 && (
<FormRow verticallyCentered centered>

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { doFetchFeaturedUris, doFetchRewardedContent } from 'redux/actions/content';
import { selectFeaturedUris, selectFetchingFeaturedUris } from 'redux/selectors/content';
import { selectFeaturedUris, selectFetchingFeaturedUris, doFetchFeaturedUris } from 'lbry-redux';
import { doFetchRewardedContent } from 'lbryinc';
import DiscoverPage from './view';
const select = state => ({

View file

@ -145,12 +145,11 @@ class FilePage extends React.Component<Props> {
if (channelName && channelClaimId) {
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
}
const speechSharable =
const speechShareable =
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
@ -184,21 +183,20 @@ class FilePage extends React.Component<Props> {
))}
<div className="card__content">
<div className="card__title-identity--file">
<h1 className="card__title card__title--file">{title}</h1>
<div className="card__title__space-between">
<h1>{title}</h1>
<div className="card__title-identity-icons">
{isRewardContent && <Icon iconColor="red" tooltip="bottom" icon={icons.FEATURED} />}
{isRewardContent && (
<Icon size={20} iconColor="red" tooltip="bottom" icon={icons.FEATURED} />
)}
<FilePrice filePage uri={normalizeURI(uri)} />
</div>
</div>
<span className="card__subtitle card__subtitle--file">
{__('Published on')}&nbsp;
<span className="card__subtitle">
<UriIndicator uri={uri} link /> {__('published on')}{' '}
<DateTime block={height} show={DateTime.SHOW_DATE} />
</span>
{metadata.nsfw && <div>NSFW</div>}
<div className="card__channel-info">
<UriIndicator uri={uri} link />
</div>
<div className="card__actions card__actions--no-margin card__actions--between">
<div className="card__actions">
{claimIsMine ? (
@ -222,7 +220,7 @@ class FilePage extends React.Component<Props> {
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
/>
)}
{speechSharable && (
{speechShareable && (
<Button
button="alt"
icon={icons.GLOBE}
@ -237,7 +235,7 @@ class FilePage extends React.Component<Props> {
<FileActions uri={uri} claimId={claim.claim_id} />
</div>
</div>
<FormRow padded>
<FormRow>
<ToolTip direction="right" body={__('Automatically download and play free content.')}>
<FormField
name="autoplay"
@ -248,7 +246,7 @@ class FilePage extends React.Component<Props> {
/>
</ToolTip>
</FormRow>
<div className="card__content--extra-padding">
<div className="card__content">
<FileDetails uri={uri} />
</div>
</div>

View file

@ -199,7 +199,7 @@ class HelpPage extends React.PureComponent<Props, State> {
)}
{this.state.uiVersion && ver ? (
<table className="table table--stretch table--help">
<table className="card__content table table--stretch table--help">
<tbody>
<tr>
<td>{__('App')}</td>

View file

@ -42,8 +42,8 @@ class ShowPage extends React.PureComponent<Props> {
if ((isResolvingUri && !claim) || !claim) {
const { claimName } = parseURI(uri);
innerContent = (
<Page>
<section className="card">
<Page notContained>
<section>
<h1>{claimName}</h1>
<div className="card__content">
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}

View file

@ -13,7 +13,6 @@ import {
doHideNotification,
} from 'lbry-redux';
import Native from 'native';
import { doFetchRewardedContent } from 'redux/actions/content';
import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthNavigate } from 'redux/actions/navigation';
import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions';
@ -27,7 +26,7 @@ import {
selectRemoteVersion,
selectUpgradeTimer,
} from 'redux/selectors/app';
import { doAuthenticate } from 'lbryinc';
import { doAuthenticate, doFetchRewardedContent } from 'lbryinc';
import { lbrySettings as config, version as appVersion } from 'package.json';
const { autoUpdater } = remote.require('electron-updater');

View file

@ -6,7 +6,6 @@ import { doNavigate } from 'redux/actions/navigation';
import {
setSubscriptionLatest,
setSubscriptionNotification,
setSubscriptionNotifications,
} from 'redux/actions/subscriptions';
import { selectNotifications } from 'redux/selectors/subscriptions';
import { selectBadgeNumber } from 'redux/selectors/app';
@ -16,8 +15,6 @@ import {
Lbry,
Lbryapi,
buildURI,
batchActions,
doResolveUris,
doFetchClaimListMine,
makeSelectCostInfoForUri,
makeSelectFileInfoForUri,
@ -31,74 +28,9 @@ import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/sel
import setBadge from 'util/setBadge';
import setProgressBar from 'util/setProgressBar';
import analytics from 'analytics';
import { Lbryio } from 'lbryinc';
const DOWNLOAD_POLL_INTERVAL = 250;
export function doFetchFeaturedUris() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_FEATURED_CONTENT_STARTED,
});
const success = ({ Uris }) => {
const urisToResolve = Object.keys(Uris).reduce(
(resolve, category) => [...resolve, ...Uris[category]],
[]
);
const actions = [
doResolveUris(urisToResolve),
{
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
uris: Uris,
success: true,
},
},
];
dispatch(batchActions(...actions));
};
const failure = () => {
dispatch({
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
uris: {},
},
});
};
Lbryio.call('file', 'list_homepage').then(success, failure);
};
}
export function doFetchRewardedContent() {
return dispatch => {
const success = nameToClaimId => {
dispatch({
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: Object.values(nameToClaimId),
success: true,
},
});
};
const failure = () => {
dispatch({
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: [],
success: false,
},
});
};
Lbryio.call('reward', 'list_featured').then(success, failure);
};
}
export function doUpdateLoadStatus(uri, outpoint) {
return (dispatch, getState) => {
Lbry.file_list({
@ -360,13 +292,13 @@ export function doPurchaseUri(uri, specificCostInfo, shouldRecordViewEvent) {
}
export function doFetchClaimsByChannel(uri, page) {
return (dispatch, getState) => {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri, page },
});
Lbry.claim_list_by_channel({ uri, page: page || 1 }).then(result => {
Lbry.claim_list_by_channel({ uri, page: page || 1, page_size: 48 }).then(result => {
const claimResult = result[uri] || {};
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult;

View file

@ -8,6 +8,14 @@ export function doNavigate(path, params = {}, options = {}) {
return;
}
// ensure uri always has "lbry://" prefix
const navigationParams = params;
if (path === '/show') {
if (navigationParams.uri && !navigationParams.uri.startsWith('lbry://')) {
navigationParams.uri = `lbry://${navigationParams.uri}`;
}
}
let url = path;
if (params && Object.values(params).length) {
url += `?${toQueryString(params)}`;

View file

@ -18,25 +18,13 @@ import { selectosNotificationsEnabled } from 'redux/selectors/settings';
import { doNavigate } from 'redux/actions/navigation';
import fs from 'fs';
import path from 'path';
import { CC_LICENSES, COPYRIGHT, OTHER } from 'constants/licenses';
type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH };
type PromiseAction = Promise<Action>;
type Dispatch = (action: Action | PromiseAction | Array<Action>) => any;
type GetState = () => {};
export const doClearPublish = () => (dispatch: Dispatch): PromiseAction => {
dispatch({ type: ACTIONS.CLEAR_PUBLISH });
return dispatch(doResetThumbnailStatus());
};
export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (
dispatch: Dispatch
): UpdatePublishFormAction =>
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: { ...publishFormValue },
});
export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction => {
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
@ -73,6 +61,19 @@ export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction
);
};
export const doClearPublish = () => (dispatch: Dispatch): PromiseAction => {
dispatch({ type: ACTIONS.CLEAR_PUBLISH });
return dispatch(doResetThumbnailStatus());
};
export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (
dispatch: Dispatch
): UpdatePublishFormAction =>
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: { ...publishFormValue },
});
export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => {
const thumbnail = fs.readFileSync(filePath);
const fileExt = path.extname(filePath);
@ -164,15 +165,27 @@ export const doPrepareEdit = (claim: any, uri: string) => (dispatch: Dispatch) =
description,
fee,
language,
licenseType: license,
licenseUrl,
nsfw,
thumbnail,
title,
uri,
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
licenseUrl,
};
// Make sure custom liscence's are mapped properly
if (!CC_LICENSES.some(({ value }) => value === license)) {
if (!licenseUrl) {
publishData.licenseType = COPYRIGHT;
} else {
publishData.licenseType = OTHER;
}
publishData.otherLicenseDescription = license;
} else {
publishData.licenseType = license;
}
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
};

View file

@ -28,7 +28,6 @@ type PublishState = {
bidError: ?string,
otherLicenseDescription: string,
licenseUrl: string,
copyrightNotice: string,
pendingPublishes: Array<any>,
};
@ -54,7 +53,6 @@ export type UpdatePublishFormData = {
bidError?: string,
otherLicenseDescription?: string,
licenseUrl?: string,
copyrightNotice?: string,
};
export type UpdatePublishFormAction = {
@ -114,9 +112,8 @@ const defaultState: PublishState = {
bid: 0.1,
bidError: undefined,
licenseType: 'None',
otherLicenseDescription: '',
otherLicenseDescription: 'All rights reserved',
licenseUrl: '',
copyrightNotice: 'All rights reserved',
publishing: false,
publishSuccess: false,
publishError: undefined,

View file

@ -4,13 +4,6 @@ import { HISTORY_ITEMS_PER_PAGE } from 'constants/content';
export const selectState = state => state.content || {};
export const selectFeaturedUris = createSelector(selectState, state => state.featuredUris);
export const selectFetchingFeaturedUris = createSelector(
selectState,
state => state.fetchingFeaturedContent
);
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
export const selectChannelClaimCounts = createSelector(

View file

@ -39,7 +39,7 @@ html {
}
body {
font-family: 'metropolis-semibold';
font-family: 'metropolis-medium';
font-weight: 400;
font-size: 16px;
line-height: 1.5;
@ -74,7 +74,6 @@ input {
line-height: 1;
cursor: text;
background-color: transparent;
font-family: 'metropolis-medium';
&[type='radio'],
&[type='checkbox'],
@ -92,6 +91,7 @@ input {
color: var(--input-copyable-color);
padding: 10px 16px;
border: 1px dashed var(--input-copyable-border);
text-overflow: ellipsis;
}
&:not(.input-copyable):not(.wunderbar__input):not(:placeholder-shown):not(:disabled) {
@ -109,7 +109,6 @@ input {
}
textarea {
font-family: 'metropolis-medium';
border: 1px solid var(--color-divider);
font-size: 0.8em;
width: 100%;
@ -152,8 +151,6 @@ dd {
}
p {
font-family: 'metropolis-medium';
&:not(:first-of-type) {
margin-top: $spacing-vertical * 1/3;
}
@ -227,7 +224,6 @@ p {
.page__empty {
margin-top: 200px;
text-align: center;
font-family: 'metropolis-medium';
display: flex;
flex-direction: column;
align-items: center;
@ -262,7 +258,7 @@ p {
}
.credit-amount {
font-size: 10px;
font-size: 0.9em;
white-space: nowrap;
}
@ -300,7 +296,6 @@ p {
color: inherit;
font-weight: inherit;
font-size: inherit;
font-family: 'metropolis-medium';
padding: 0;
}
@ -333,10 +328,6 @@ p {
-webkit-box-orient: vertical;
}
.busy-indicator {
font-family: 'metropolis-medium';
}
.busy-indicator__loader {
background: url('../../../static/img/busy.gif') no-repeat center center;
display: inline-block;
@ -355,7 +346,6 @@ p {
.help {
font-size: 12px;
font-family: 'metropolis-medium';
color: var(--color-help);
}
@ -364,7 +354,6 @@ p {
}
.meta {
font-family: 'metropolis-medium';
font-size: 0.8em;
color: var(--color-meta-light);
}

View file

@ -52,6 +52,8 @@ $large-breakpoint: 1921px;
--color-search-placeholder: var(--color-placeholder);
--color-credit-free: var(--color-dark-blue);
--color-credit-price: var(--card-text-color);
--color-text-black: #444;
--color-text-white: #efefef;
/* Shadows */
--box-shadow-layer: transparent; // 0 2px 4px rgba(0,0,0,0.25);
@ -60,8 +62,8 @@ $large-breakpoint: 1921px;
--box-shadow-header: 0px 6px 20px 1px rgba(0, 0, 0, 0.05);
/* Text */
--text-color: var(--color-black);
--text-color-inverse: var(--color-white);
--text-color: var(--color-text-black);
--text-color-inverse: var(--color-text-white);
--text-help-color: var(--color-help);
--text-max-width: 660px;
--text-link-padding: 4px;

View file

@ -29,3 +29,6 @@
@import 'component/_toggle.scss';
@import 'component/_search.scss';
@import 'component/_dat-gui.scss';
@import 'component/_item-list.scss';
@import 'component/_time.scss';
@import 'component/_icon.scss';

View file

@ -19,7 +19,6 @@ button:disabled {
fill: currentColor; // for proper icon color
font-size: 12px;
transition: all var(--animation-duration) var(--animation-style);
font-family: 'metropolis-medium';
&:not(:disabled) {
box-shadow: var(--box-shadow-button);
@ -161,6 +160,7 @@ button:disabled {
.btn--uri-indicator {
transition: color var(--animation-duration) var(--animation-style);
display: inline-block;
&:hover {
color: var(--btn-color-inverse);
@ -172,7 +172,6 @@ button:disabled {
}
.btn.btn--header-balance {
font-family: 'metropolis-medium';
font-size: 14px;
color: var(--header-primary-color);

View file

@ -6,10 +6,18 @@
display: flex;
flex-direction: column;
max-width: var(--card-max-width);
}
white-space: normal;
.card > h1 {
word-wrap: break-word;
.card__media {
padding-top: var(--video-aspect-ratio);
}
// Text that is shown if a piece of content has no thumbnail
// We need to do this because the thumbnail uses padding-top: var(--video-aspect-ratio); for dynamic height
// this lets the text sit in the middle instead of the bottom
.card__media-text {
margin-top: calc(var(--video-aspect-ratio) * -1);
}
}
.card--section {
@ -20,25 +28,25 @@
}
.card--small {
white-space: normal;
font-size: 13px;
}
.card__media {
padding-top: var(--video-aspect-ratio);
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: var(--color-placeholder);
}
.card__media-text {
// for the weird padding required for dynamic height
// this lets the text sit in the middle instead of the bottom
margin-top: calc(var(--video-aspect-ratio) * -1);
}
.card__media--no-img {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.channel-name {
font-size: 12px;
@media only screen and (min-width: $medium-breakpoint) {
font-size: 14px;
}
}
.card__media--nsfw {
background-color: var(--color-error);
}
.card--link {
@ -66,31 +74,29 @@
pointer-events: none;
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: var(--color-placeholder);
}
.card__media--no-img {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.card__media--nsfw {
background-color: var(--color-error);
}
.card__title-identity {
.card__title {
font-size: 1.5em;
color: var(--text-color);
padding-top: $spacing-vertical * 1/3;
@media (min-width: $large-breakpoint) {
font-size: 1.7em;
}
}
.card__title-identity--file {
.card__title--file-card {
// FileCard is slightly different where the title is only slightly bigger than the subtitle
// Slightly bigger than 2 lines for consistent channel placement
font-size: 1.1em;
height: 4em;
@media only screen and (min-width: $large-breakpoint) {
font-size: 1.3em;
}
}
.card__title__space-between {
display: flex;
align-items: center;
justify-content: space-between;
.icon {
@ -104,73 +110,26 @@
align-self: flex-start;
}
.card__title {
font-size: 18px;
color: var(--text-color);
}
.card__title--small {
font-size: 12px;
line-height: 12px;
@media only screen and (min-width: $medium-breakpoint) {
font-size: 14px;
line-height: 14px;
}
}
.card__title--x-small {
font-size: 12px;
line-height: 12px;
}
.card__title--large {
font-size: 22px;
}
.card__title--file {
font-family: 'metropolis-bold';
font-size: 28px;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 5px;
font-size: 18px;
}
.card__title--file-card {
padding-top: $spacing-vertical * 1/3;
// height is the same height that two lines of title fill
// doing this so content below the title is inline accross the row
height: 30px;
@media only screen and (min-width: $medium-breakpoint) {
height: 36px;
}
}
.card__subtitle {
margin: 0;
font-size: 14px;
font-family: 'metropolis-medium';
color: var(--card-text-color);
font-size: 1em;
line-height: 1em;
@media (min-width: $large-breakpoint) {
font-size: 1.2em;
}
}
.card__subtitle--x-small {
font-size: 12px;
.card__subtext-title {
font-size: 1.1em;
&:not(:first-of-type) {
margin-top: $spacing-vertical * 3/2;
}
}
.card__subtitle--large {
font-size: 18px;
padding-bottom: $spacing-vertical * 1/3;
}
.card__subtitle-price {
padding-top: $spacing-vertical * 1/3;
}
.card__title--small + .card__subtitle,
.card__title--x-small + .card__subtitle {
padding-top: $spacing-vertical * 1/3;
.card__subtext {
font-size: 0.85em;
}
.card__meta {
@ -185,11 +144,6 @@
align-items: center;
padding-top: $spacing-vertical * 1/3;
color: var(--card-text-color);
.icon + .icon,
.credit-amount + .icon {
margin-left: $spacing-vertical * 1/3;
}
}
// .card-media__internal__links should always be inside a card
@ -201,58 +155,10 @@
}
}
.card--small {
.card-media__internal-links {
top: $spacing-vertical * 1/3;
right: $spacing-vertical * 1/3;
}
}
// Channel info with buttons on the right side
.card__channel-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-width 0 0;
}
.card__channel-info--large {
padding-top: 0;
padding-bottom: $spacing-vertical * 2/3;
}
.card__content {
margin-top: $spacing-vertical * 2/3;
}
.card__content--extra-padding {
margin-top: $spacing-vertical * 3/2;
}
.card__subtext-title {
color: var(--text-color);
font-size: calc(var(--font-size-subtext-multiple) * 1.2em);
&:not(:first-of-type) {
margin-top: $spacing-vertical * 3/2;
}
}
.card__subtext {
font-size: calc(var(--font-size-subtext-multiple) * 1em);
padding-top: $spacing-vertical * 1/3;
word-break: break-word;
font-family: 'metropolis-medium';
}
.card__subtext--small {
font-size: calc(var(--font-size-subtext-multiple) * 0.8em);
}
.card__subtext--large {
font-size: calc(var(--font-size-subtext-multiple) * 0.9em);
}
.card__actions {
margin-top: $spacing-vertical * 2/3;
display: flex;
@ -335,6 +241,7 @@
}
.card-row__title {
font-family: 'metropolis-semibold';
display: flex;
align-items: center;
font-size: 18px;
@ -462,43 +369,3 @@
padding: $spacing-vertical * 1/3;
margin: $spacing-vertical * 1/3 0;
}
.card__media--autothumb {
color: red !important;
}
.card__media {
&.card__media--autothumb.purple {
background-color: #9c27b0;
}
&.card__media--autothumb.red {
background-color: #e53935;
}
&.card__media--autothumb.pink {
background-color: #e91e63;
}
&.card__media--autothumb.indigo {
background-color: #3f51b5;
}
&.card__media--autothumb.blue {
background-color: #2196f3;
}
&.card__media--autothumb.light-blue {
background-color: #039be5;
}
&.card__media--autothumb.cyan {
background-color: #00acc1;
}
&.card__media--autothumb.teal {
background-color: #009688;
}
&.card__media--autothumb.green {
background-color: #43a047;
}
&.card__media--autothumb.yellow {
background-color: #ffeb3b;
}
&.card__media--autothumb.orange {
background-color: #ffa726;
}
}

View file

@ -4,7 +4,3 @@
white-space: nowrap;
text-overflow: ellipsis;
}
.channel-indicator__icon--invalid {
color: var(--color-error);
}

View file

@ -95,10 +95,6 @@
&.content__empty--nsfw {
background-color: var(--color-nsfw);
}
.card__media-text {
margin-top: calc(var(--video-aspect-ratio) * -1);
}
}
img {

View file

@ -22,25 +22,32 @@
.file-tile {
display: flex;
font-size: 14px;
padding-top: $spacing-vertical;
.card__media {
height: var(--file-tile-media-height);
flex: 0 0 var(--file-tile-media-width);
}
}
.card__subtitle {
line-height: 1;
display: flex;
align-items: center;
.file-tile--small {
font-size: 12px;
.card__media {
height: var(--file-tile-media-height-small);
flex: 0 0 var(--file-tile-media-width-small);
}
}
.file-tile__channel {
padding-right: $spacing-width * 1/4;
.file-tile--large {
font-size: 16px;
}
.file-tile.file-tile--small {
.file-tile__title {
}
.file-tile--small {
padding-top: $spacing-vertical * 2/3;
.card__media {
@ -49,7 +56,7 @@
}
}
.file-tile.file-tile--large {
.file-tile--large {
.card__media {
height: var(--file-tile-media-height-large);
flex: 0 0 var(--file-tile-media-width-large);

View file

@ -0,0 +1,6 @@
// Icons with icons directly following should have a margin-right for proper spacing
// Same for prices on cards
.icon + .icon,
.credit-amount + .icon {
margin-left: $spacing-vertical * 1/3;
}

View file

@ -0,0 +1,26 @@
.item-list {
background-color: var(--card-bg);
margin-top: $spacing-vertical;
}
.item-list__item {
display: flex;
align-items: center;
padding: $spacing-vertical * 1/3;
input,
.item-list__item--cutoff {
margin-right: $spacing-vertical;
}
}
.item-list__item--selected {
background-color: var(--table-item-odd);
}
.item-list__item--cutoff {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
max-width: 350px;
}

View file

@ -39,7 +39,6 @@
/* Paragraphs */
p {
margin: $spacing-vertical * 2/3 0;
white-space: pre-line;
}

View file

@ -24,7 +24,6 @@
padding: 10px;
padding-left: 30px;
font-size: 13px;
font-family: 'metropolis-medium';
display: flex;
align-items: center;
justify-content: center;
@ -54,7 +53,6 @@
flex-direction: row;
justify-items: flex-start;
align-items: center;
font-family: 'metropolis-medium';
&:not(:first-of-type) {
border-top: 1px solid var(--search-item-border-color);
@ -75,7 +73,6 @@
.wunderbar__suggestion-label--action {
margin-left: $spacing-vertical * 1/3;
white-space: nowrap;
font-family: 'metropolis-medium';
font-size: 12px;
line-height: 0.1; // to vertically align because the font size is smaller
}

View file

@ -3,7 +3,6 @@ table.table,
word-wrap: break-word;
max-width: 100%;
text-align: left;
margin-top: $spacing-vertical * 2/3;
tr td:first-of-type,
tr th:first-of-type {
@ -107,48 +106,3 @@ table.table--transactions {
width: 15%;
}
}
table.table--history {
margin-top: $spacing-vertical * 1/3;
tbody {
tr {
&:nth-child(even),
&:nth-child(odd) {
background-color: var(--table-item-even);
&.history__selected {
color: red;
background-color: var(--table-item-odd);
}
}
}
td {
cursor: default;
padding: $spacing-vertical * 1/3 0;
}
td:nth-of-type(1) {
width: 7.5%;
}
td:nth-of-type(2) {
width: 17.5%;
}
td:nth-of-type(3) {
width: 40%;
max-width: 30vw;
padding-right: $spacing-vertical * 2/3;
}
td:nth-of-type(4) {
width: 30%;
}
td:nth-of-type(3),
td:nth-of-type(4) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

View file

@ -0,0 +1,9 @@
// All CSS for date & time ui
.time {
color: var(--color-help);
}
.time--ago {
min-width: 160px;
}

View file

@ -1,5 +1,5 @@
:root {
/* Colors */
--color-divider: #53637C;;
--color-canvas: transparent;
@ -12,7 +12,7 @@
--color-credit-free: var(--color-secondary);
/* Text */
--text-color: var(--color-white);
--text-color: var(--color-text-white);
--text-help-color: var(--color-help);
/* Form */