Markdown Support

This commit is contained in:
Le Long 2017-06-15 21:30:56 +02:00 committed by 6ea86b96
parent 8325828f6e
commit b09d71ecff
9 changed files with 224 additions and 110 deletions

1
.gitignore vendored
View file

@ -26,3 +26,4 @@ build/daemon.zip
.vimrc .vimrc
package-lock.json package-lock.json
ui/yarn.lock

View file

@ -10,14 +10,17 @@ Web UI version numbers should always match the corresponding version of LBRY App
### Added ### Added
* Added option to release claim when deleting a file * Added option to release claim when deleting a file
* Added transition to card hovers to smooth animation * Added transition to card hovers to smooth animation
* Support markdown makeup in claim description
*
### Changed ### Changed
* * Publishes now uses claims rather than files
* *
### Fixed ### Fixed
* Fixed bug with download notice when switching window focus * Fixed bug with download notice when switching window focus
* * Fixed newly published files appearing twice
* Fixed unconfirmed published files missing channel name
### Deprecated ### Deprecated
* *

View file

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import ReactDOMServer from "react-dom/server";
import lbry from "../lbry.js"; import lbry from "../lbry.js";
import ReactMarkdown from "react-markdown";
//component/icon.js //component/icon.js
export class Icon extends React.PureComponent { export class Icon extends React.PureComponent {

View file

@ -1,7 +1,12 @@
import React from "react"; import React from "react";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import Link from "component/link"; import Link from "component/link";
import { TruncatedText, Icon } from "component/common"; import {
Thumbnail,
TruncatedText,
Icon,
TruncatedMarkdown,
} from "component/common";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
import UriIndicator from "component/uriIndicator"; import UriIndicator from "component/uriIndicator";
import NsfwOverlay from "component/nsfwOverlay"; import NsfwOverlay from "component/nsfwOverlay";
@ -94,7 +99,7 @@ class FileCard extends React.PureComponent {
style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }} style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}
/>} />}
<div className="card__content card__subtext card__subtext--two-lines"> <div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>{description}</TruncatedText> <TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
</div> </div>
</Link> </Link>
</div> </div>

View file

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import FileSelector from "./file-selector.js"; import FileSelector from "./file-selector.js";
import { Icon } from "./common.js"; import SimpleMDE from "react-simplemde-editor";
import style from "react-simplemde-editor/dist/simplemde.min.css";
var formFieldCounter = 0, let formFieldCounter = 0,
formFieldFileSelectorTypes = ["file", "directory"], formFieldFileSelectorTypes = ["file", "directory"],
formFieldNestedLabelTypes = ["radio", "checkbox"]; formFieldNestedLabelTypes = ["radio", "checkbox"];
@ -24,6 +25,7 @@ export class FormField extends React.PureComponent {
this._fieldRequiredText = __("This field is required"); this._fieldRequiredText = __("This field is required");
this._type = null; this._type = null;
this._element = null; this._element = null;
this._extraElementProps = {};
this.state = { this.state = {
isError: null, isError: null,
@ -38,6 +40,12 @@ export class FormField extends React.PureComponent {
} else if (this.props.type == "text-number") { } else if (this.props.type == "text-number") {
this._element = "input"; this._element = "input";
this._type = "text"; this._type = "text";
} else if (this.props.type == "SimpleMDE") {
this._element = SimpleMDE;
this._type = "textarea";
this._extraElementProps.options = {
hideIcons: ["guide", "heading", "image", "fullscreen"],
};
} else if (formFieldFileSelectorTypes.includes(this.props.type)) { } else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = "input"; this._element = "input";
this._type = "hidden"; this._type = "hidden";
@ -81,6 +89,8 @@ export class FormField extends React.PureComponent {
getValue() { getValue() {
if (this.props.type == "checkbox") { if (this.props.type == "checkbox") {
return this.refs.field.checked; return this.refs.field.checked;
} else if (this.props.type == "SimpleMDE") {
return this.refs.field.simplemde.value();
} else { } else {
return this.refs.field.value; return this.refs.field.value;
} }
@ -90,6 +100,10 @@ export class FormField extends React.PureComponent {
return this.refs.field.options[this.refs.field.selectedIndex]; return this.refs.field.options[this.refs.field.selectedIndex];
} }
getOptions() {
return this.refs.field.options;
}
render() { render() {
// Pass all unhandled props to the field element // Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props), const otherProps = Object.assign({}, this.props),
@ -106,7 +120,6 @@ export class FormField extends React.PureComponent {
delete otherProps.className; delete otherProps.className;
delete otherProps.postfix; delete otherProps.postfix;
delete otherProps.prefix; delete otherProps.prefix;
const element = ( const element = (
<this._element <this._element
id={elementId} id={elementId}
@ -122,6 +135,7 @@ export class FormField extends React.PureComponent {
(isError ? "form-field__input--error" : "") (isError ? "form-field__input--error" : "")
} }
{...otherProps} {...otherProps}
{...this._extraElementProps}
> >
{this.props.children} {this.props.children}
</this._element> </this._element>
@ -220,6 +234,10 @@ export class FormRow extends React.PureComponent {
return this.refs.field.getSelectedElement(); return this.refs.field.getSelectedElement();
} }
getOptions() {
return this.refs.field.getOptions();
}
focus() { focus() {
this.refs.field.focus(); this.refs.field.focus();
} }

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import ReactMarkdown from "react-markdown";
import lbry from "lbry.js"; import lbry from "lbry.js";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import Video from "component/video"; import Video from "component/video";
@ -119,7 +120,11 @@ class FilePage extends React.PureComponent {
</div> </div>
</div> </div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines"> <div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata && metadata.description} <ReactMarkdown
source={(metadata && metadata.description) || ""}
escapeHtml={true}
disallowedTypes={["Heading", "HtmlInline", "HtmlBlock"]}
/>
</div> </div>
</div> </div>
{metadata {metadata

View file

@ -5,13 +5,16 @@ import { FormField, FormRow } from "component/form.js";
import Link from "component/link"; import Link from "component/link";
import rewards from "rewards"; import rewards from "rewards";
import Modal from "component/modal"; import Modal from "component/modal";
import Notice from "component/notice";
import { BusyMessage } from "component/common"; import { BusyMessage } from "component/common";
class PublishPage extends React.PureComponent { class PublishPage extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this._requiredFields = ["meta_title", "name", "bid", "tos_agree"]; this._requiredFields = ["name", "bid", "meta_title", "tosAgree"];
this._defaultCopyrightNotice = "All rights reserved.";
this.state = { this.state = {
rawName: "", rawName: "",
@ -23,11 +26,17 @@ class PublishPage extends React.PureComponent {
channel: "anonymous", channel: "anonymous",
newChannelName: "@", newChannelName: "@",
newChannelBid: 10, newChannelBid: 10,
myClaimValue: 0.0, meta_title: "",
myClaimMetadata: null, meta_thumbnail: "",
copyrightNotice: "", meta_description: "",
meta_language: "en",
meta_nsfw: "0",
licenseType: "",
copyrightNotice: this._defaultCopyrightNotice,
otherLicenseDescription: "", otherLicenseDescription: "",
otherLicenseUrl: "", otherLicenseUrl: "",
tosAgree: false,
prefillDone: false,
uploadProgress: 0.0, uploadProgress: 0.0,
uploaded: false, uploaded: false,
errorMessage: null, errorMessage: null,
@ -80,36 +89,18 @@ class PublishPage extends React.PureComponent {
return; return;
} }
if (this.state.nameIsMine) { let metadata = {};
// Pre-populate with existing metadata
var metadata = Object.assign({}, this.state.myClaimMetadata);
if (this.refs.file.getValue() !== "") {
delete metadata.sources;
}
} else {
var metadata = {};
}
for (let metaField of [ for (let metaField of ["title", "description", "thumbnail", "language"]) {
"title", const value = this.state["meta_" + metaField];
"description", if (value) {
"thumbnail",
"license",
"license_url",
"language",
]) {
var value = this.refs["meta_" + metaField].getValue();
if (value !== "") {
metadata[metaField] = value; metadata[metaField] = value;
} }
} }
metadata.nsfw = parseInt(this.refs.meta_nsfw.getValue()) === 1; metadata.license = this.getLicense();
metadata.licenseUrl = this.getLicenseUrl();
const licenseUrl = this.refs.meta_license_url.getValue(); metadata.nsfw = !!parseInt(this.state.meta_nsfw);
if (licenseUrl) {
metadata.license_url = licenseUrl;
}
var doPublish = () => { var doPublish = () => {
var publishArgs = { var publishArgs = {
@ -203,6 +194,8 @@ class PublishPage extends React.PureComponent {
} }
myClaimInfo() { myClaimInfo() {
const { name } = this.state;
return Object.values(this.props.myClaims).find( return Object.values(this.props.myClaims).find(
claim => claim.name === name claim => claim.name === name
); );
@ -240,6 +233,7 @@ class PublishPage extends React.PureComponent {
this.setState({ this.setState({
rawName: rawName, rawName: rawName,
name: name, name: name,
prefillDone: false,
uri, uri,
}); });
@ -254,6 +248,43 @@ class PublishPage extends React.PureComponent {
}); });
} }
handlePrefillClicked() {
const {license, licenseUrl, title, thumbnail, description,
language, nsfw} = this.myClaimInfo().value.stream.metadata;
let newState = {
meta_title: title,
meta_thumbnail: thumbnail,
meta_description: description,
meta_language: language,
meta_nsfw: nsfw,
};
if (license == this._defaultCopyrightNotice) {
newState.licenseType = "copyright";
newState.copyrightNotice = this._defaultCopyrightNotice;
} else {
// If the license URL or description matches one of the drop-down options, use that
let licenseType = "other"; // Will be overridden if we find a match
for (let option of this._meta_license.getOptions()) {
if (
option.getAttribute("data-url") === licenseUrl ||
option.text === license
) {
licenseType = option.value;
}
}
if (licenseType == "other") {
newState.otherLicenseDescription = license;
newState.otherLicenseUrl = licenseUrl;
}
newState.licenseType = licenseType;
}
this.setState(newState);
}
handleBidChange(event) { handleBidChange(event) {
this.setState({ this.setState({
bid: event.target.value, bid: event.target.value,
@ -278,20 +309,21 @@ class PublishPage extends React.PureComponent {
}); });
} }
handleLicenseChange(event) { handleMetadataChange(event) {
var licenseType = event.target.options[ /**
event.target.selectedIndex * This function is used for all metadata inputs that store the final value directly into state.
].getAttribute("data-license-type"); * The only exceptions are inputs related to license description and license URL, which require
var newState = { * more complex logic and the final value is determined at submit time.
copyrightChosen: licenseType == "copyright", */
otherLicenseChosen: licenseType == "other", this.setState({
}; ["meta_" + event.target.name]: event.target.value,
});
}
if (licenseType == "copyright") { handleLicenseTypeChange(event) {
newState.copyrightNotice = __("All rights reserved."); this.setState({
} licenseType: event.target.value,
});
this.setState(newState);
} }
handleCopyrightNoticeChange(event) { handleCopyrightNoticeChange(event) {
@ -322,7 +354,7 @@ class PublishPage extends React.PureComponent {
handleTOSChange(event) { handleTOSChange(event) {
this.setState({ this.setState({
TOSAgreed: event.target.checked, tosAgree: event.target.checked,
}); });
} }
@ -366,16 +398,25 @@ class PublishPage extends React.PureComponent {
); );
} }
getLicense() {
switch (this.state.licenseType) {
case "copyright":
return this.state.copyrightNotice;
case "other":
return this.state.otherLicenseDescription;
default:
return this._meta_license.getSelectedElement().text;
}
}
getLicenseUrl() { getLicenseUrl() {
if (!this.refs.meta_license) { switch (this.state.licenseType) {
return ""; case "copyright":
} else if (this.state.otherLicenseChosen) { return "";
return this.state.otherLicenseUrl; case "other":
} else { return this.state.otherLicenseUrl;
return ( default:
this.refs.meta_license.getSelectedElement().getAttribute("data-url") || return this._meta_license.getSelectedElement().getAttribute("data-url");
""
);
} }
} }
@ -398,7 +439,7 @@ class PublishPage extends React.PureComponent {
this.props.resolvingUris.indexOf(this.state.uri) !== -1 && this.props.resolvingUris.indexOf(this.state.uri) !== -1 &&
this.claim() === undefined this.claim() === undefined
) { ) {
return <BusyMessage />; return __("Checking...");
} else if (!this.state.name) { } else if (!this.state.name) {
return __("Select a URL for this publish."); return __("Select a URL for this publish.");
} else if (!this.claim()) { } else if (!this.claim()) {
@ -482,43 +523,55 @@ class PublishPage extends React.PureComponent {
} }
/> />
</div> </div>
{!this.state.hasFile {!this.state.hasFile && !this.myClaimExists()
? "" ? null
: <div> : <div>
<div className="card__content"> <div className="card__content">
<FormRow <FormRow
label={__("Title")} label={__("Title")}
type="text" type="text"
ref="meta_title"
name="title" name="title"
placeholder={__("Title")} value={this.state.meta_title}
placeholder="Titular Title"
onChange={event => {
this.handleMetadataChange(event);
}}
/> />
</div> </div>
<div className="card__content"> <div className="card__content">
<FormRow <FormRow
type="text" type="text"
label={__("Thumbnail URL")} label={__("Thumbnail URL")}
ref="meta_thumbnail"
name="thumbnail" name="thumbnail"
value={this.state.meta_thumbnail}
placeholder="http://spee.ch/mylogo" placeholder="http://spee.ch/mylogo"
onChange={event => {
this.handleMetadataChange(event);
}}
/> />
</div> </div>
<div className="card__content"> <div className="card__content">
<FormRow <FormRow
label={__("Description")} label={__("Description")}
type="textarea" type="SimpleMDE"
ref="meta_description" ref="meta_description"
name="description" name="description"
value={this.state.meta_description}
placeholder={__("Description of your content")} placeholder={__("Description of your content")}
onChange={event => {
this.handleMetadataChange(event);
}}
/> />
</div> </div>
<div className="card__content"> <div className="card__content">
<FormRow <FormRow
label={__("Language")} label={__("Language")}
type="select" type="select"
defaultValue="en" value={this.state.meta_language}
ref="meta_language"
name="language" name="language"
onChange={event => {
this.handleMetadataChange(event);
}}
> >
<option value="en">{__("English")}</option> <option value="en">{__("English")}</option>
<option value="zh">{__("Chinese")}</option> <option value="zh">{__("Chinese")}</option>
@ -533,9 +586,11 @@ class PublishPage extends React.PureComponent {
<FormRow <FormRow
type="select" type="select"
label={__("Maturity")} label={__("Maturity")}
defaultValue="en" value={this.state.meta_nsfw}
ref="meta_nsfw"
name="nsfw" name="nsfw"
onChange={event => {
this.handleMetadataChange(event);
}}
> >
{/* <option value=""></option> */} {/* <option value=""></option> */}
<option value="0">{__("All Ages")}</option> <option value="0">{__("All Ages")}</option>
@ -583,8 +638,7 @@ class PublishPage extends React.PureComponent {
placeholder="1.00" placeholder="1.00"
min="0.01" min="0.01"
onChange={event => this.handleFeeAmountChange(event)} onChange={event => this.handleFeeAmountChange(event)}
/> />{" "}
{" "}
<FormField <FormField
type="select" type="select"
onChange={event => { onChange={event => {
@ -605,66 +659,71 @@ class PublishPage extends React.PureComponent {
<FormRow <FormRow
label="License" label="License"
type="select" type="select"
ref="meta_license" value={this.state.licenseType}
name="license" ref={row => {
this._meta_license = row;
}}
onChange={event => { onChange={event => {
this.handleLicenseChange(event); this.handleLicenseTypeChange(event);
}} }}
> >
<option /> <option />
<option>{__("Public Domain")}</option> <option value="publicDomain">{__("Public Domain")}</option>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode"> <option
value="cc-by"
data-url="https://creativecommons.org/licenses/by/4.0/legalcode"
>
{__("Creative Commons Attribution 4.0 International")} {__("Creative Commons Attribution 4.0 International")}
</option> </option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode"> <option
value="cc-by-sa"
data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode"
>
{__( {__(
"Creative Commons Attribution-ShareAlike 4.0 International" "Creative Commons Attribution-ShareAlike 4.0 International"
)} )}
</option> </option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode"> <option
value="cc-by-nd"
data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode"
>
{__( {__(
"Creative Commons Attribution-NoDerivatives 4.0 International" "Creative Commons Attribution-NoDerivatives 4.0 International"
)} )}
</option> </option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode"> <option
value="cc-by-nc"
data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode"
>
{__( {__(
"Creative Commons Attribution-NonCommercial 4.0 International" "Creative Commons Attribution-NonCommercial 4.0 International"
)} )}
</option> </option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode"> <option
value="cc-by-nc-sa"
data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode"
>
{__( {__(
"Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International" "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International"
)} )}
</option> </option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode"> <option
value="cc-by-nc-nd"
data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode"
>
{__( {__(
"Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International" "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International"
)} )}
</option> </option>
<option <option value="copyright">
data-license-type="copyright"
{...(this.state.copyrightChosen
? { value: this.state.copyrightNotice }
: {})}
>
{__("Copyrighted...")} {__("Copyrighted...")}
</option> </option>
<option <option value="other">
data-license-type="other"
{...(this.state.otherLicenseChosen
? { value: this.state.otherLicenseDescription }
: {})}
>
{__("Other...")} {__("Other...")}
</option> </option>
</FormRow> </FormRow>
<FormField
type="hidden" {this.state.licenseType == "copyright"
ref="meta_license_url"
name="license_url"
value={this.getLicenseUrl()}
/>
{this.state.copyrightChosen
? <FormRow ? <FormRow
label={__("Copyright notice")} label={__("Copyright notice")}
type="text" type="text"
@ -675,21 +734,25 @@ class PublishPage extends React.PureComponent {
}} }}
/> />
: null} : null}
{this.state.otherLicenseChosen
{this.state.licenseType == "other"
? <FormRow ? <FormRow
label={__("License description")} label={__("License description")}
type="text" type="text"
name="other-license-description" name="other-license-description"
value={this.state.otherLicenseDescription}
onChange={event => { onChange={event => {
this.handleOtherLicenseDescriptionChange(); this.handleOtherLicenseDescriptionChange(event);
}} }}
/> />
: null} : null}
{this.state.otherLicenseChosen
{this.state.licenseType == "other"
? <FormRow ? <FormRow
label={__("License URL")} label={__("License URL")}
type="text" type="text"
name="other-license-url" name="other-license-url"
value={this.state.otherLicenseUrl}
onChange={event => { onChange={event => {
this.handleOtherLicenseUrlChange(event); this.handleOtherLicenseUrlChange(event);
}} }}
@ -730,6 +793,15 @@ class PublishPage extends React.PureComponent {
}} }}
helper={this.getNameBidHelpText()} helper={this.getNameBidHelpText()}
/> />
{this.myClaimExists() && !this.state.prefillDone
? <Notice>
{__("You already have a claim with this name.")}{" "}
<Link
label={__("Use data from my existing claim")}
onClick={() => this.handlePrefillClicked()}
/>
</Notice>
: null}
</div> </div>
{this.state.rawName {this.state.rawName
? <div className="card__content"> ? <div className="card__content">
@ -763,15 +835,11 @@ class PublishPage extends React.PureComponent {
<Link <Link
href="https://www.lbry.io/termsofservice" href="https://www.lbry.io/termsofservice"
label={__("LBRY terms of service")} label={__("LBRY terms of service")}
checked={this.state.TOSAgreed}
/> />
</span> </span>
} }
type="checkbox" type="checkbox"
name="tos_agree" checked={this.state.tosAgree}
ref={field => {
this.refs.tos_agree = field;
}}
onChange={event => { onChange={event => {
this.handleTOSChange(event); this.handleTOSChange(event);
}} }}

View file

@ -29,8 +29,10 @@
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",
"react": "^15.4.0", "react": "^15.4.0",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-markdown": "^2.5.0",
"react-modal": "^1.5.2", "react-modal": "^1.5.2",
"react-redux": "^5.0.3", "react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.11",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-action-buffer": "^1.1.0", "redux-action-buffer": "^1.1.0",
"redux-logger": "^3.0.1", "redux-logger": "^3.0.1",
@ -52,6 +54,8 @@
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",
"electron-rebuild": "^1.5.11",
"css-loader": "^0.28.4",
"eslint": "^3.10.2", "eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0", "eslint-config-airbnb": "^13.0.0",
"eslint-loader": "^1.6.1", "eslint-loader": "^1.6.1",
@ -64,6 +68,7 @@
"lint-staged": "^3.6.0", "lint-staged": "^3.6.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"prettier": "^1.4.2", "prettier": "^1.4.2",
"style-loader": "^0.18.2",
"webpack": "^2.6.1", "webpack": "^2.6.1",
"webpack-dev-server": "^2.4.4", "webpack-dev-server": "^2.4.4",
"webpack-notifier": "^1.5.0", "webpack-notifier": "^1.5.0",

View file

@ -117,6 +117,9 @@ input[type="text"].input-copyable {
border: $width-input-border solid $color-form-border; border: $width-input-border solid $color-form-border;
} }
} }
.form-field--SimpleMDE {
display: block;
}
.form-field__label { .form-field__label {
&[for] { cursor: pointer; } &[for] { cursor: pointer; }
@ -163,4 +166,8 @@ input[type="text"].input-copyable {
} }
.form-field__helper { .form-field__helper {
color: $color-help; color: $color-help;
}
.form-field__input.form-field__input-SimpleMDE .CodeMirror-scroll {
height: auto;
} }