add in-app text and markdown publishing
This commit is contained in:
parent
80d8eeb4cf
commit
a5d1746151
9 changed files with 354 additions and 40 deletions
|
@ -3,7 +3,6 @@ import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux';
|
||||||
import PublishPage from './view';
|
import PublishPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
title: makeSelectPublishFormValue('title')(state),
|
|
||||||
description: makeSelectPublishFormValue('description')(state),
|
description: makeSelectPublishFormValue('description')(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,7 +10,4 @@ const perform = dispatch => ({
|
||||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(PublishPage);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(PublishPage);
|
|
|
@ -7,14 +7,13 @@ import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: ?string,
|
|
||||||
description: ?string,
|
description: ?string,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishText(props: Props) {
|
function PublishText(props: Props) {
|
||||||
const { title, description, updatePublishForm, disabled } = props;
|
const { description, updatePublishForm, disabled } = props;
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('publish-form-description-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('publish-form-description-mode', false);
|
||||||
function toggleMarkdown() {
|
function toggleMarkdown() {
|
||||||
setAdvancedEditor(!advancedEditor);
|
setAdvancedEditor(!advancedEditor);
|
||||||
|
@ -24,16 +23,6 @@ function PublishText(props: Props) {
|
||||||
<Card
|
<Card
|
||||||
actions={
|
actions={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FormField
|
|
||||||
type="text"
|
|
||||||
name="content_title"
|
|
||||||
label={__('Title')}
|
|
||||||
placeholder={__('Descriptive titles work best')}
|
|
||||||
disabled={disabled}
|
|
||||||
value={title}
|
|
||||||
onChange={e => updatePublishForm({ title: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="content_description"
|
name="content_description"
|
|
@ -12,6 +12,7 @@ import PublishPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
name: makeSelectPublishFormValue('name')(state),
|
name: makeSelectPublishFormValue('name')(state),
|
||||||
|
title: makeSelectPublishFormValue('title')(state),
|
||||||
filePath: makeSelectPublishFormValue('filePath')(state),
|
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||||
optimize: makeSelectPublishFormValue('optimize')(state),
|
optimize: makeSelectPublishFormValue('optimize')(state),
|
||||||
isStillEditing: selectIsStillEditing(state),
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { regexInvalidURI } from 'lbry-redux';
|
import { regexInvalidURI } from 'lbry-redux';
|
||||||
|
import StoryEditor from 'component/storyEditor';
|
||||||
import FileSelector from 'component/common/file-selector';
|
import FileSelector from 'component/common/file-selector';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
@ -9,9 +10,13 @@ import { FormField } from 'component/common/form';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
uri: ?string,
|
||||||
|
mode: ?string,
|
||||||
name: ?string,
|
name: ?string,
|
||||||
|
title: ?string,
|
||||||
filePath: string | WebFile,
|
filePath: string | WebFile,
|
||||||
isStillEditing: boolean,
|
isStillEditing: boolean,
|
||||||
balance: number,
|
balance: number,
|
||||||
|
@ -26,11 +31,16 @@ type Props = {
|
||||||
size: number,
|
size: number,
|
||||||
duration: number,
|
duration: number,
|
||||||
isVid: boolean,
|
isVid: boolean,
|
||||||
|
setPublishMode: string => void,
|
||||||
|
setPrevFileText: string => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishFile(props: Props) {
|
function PublishFile(props: Props) {
|
||||||
const {
|
const {
|
||||||
|
uri,
|
||||||
|
mode,
|
||||||
name,
|
name,
|
||||||
|
title,
|
||||||
balance,
|
balance,
|
||||||
filePath,
|
filePath,
|
||||||
isStillEditing,
|
isStillEditing,
|
||||||
|
@ -44,11 +54,14 @@ function PublishFile(props: Props) {
|
||||||
size,
|
size,
|
||||||
duration,
|
duration,
|
||||||
isVid,
|
isVid,
|
||||||
|
setPublishMode,
|
||||||
|
setPrevFileText,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const ffmpegAvail = ffmpegStatus.available;
|
const ffmpegAvail = ffmpegStatus.available;
|
||||||
const [oversized, setOversized] = useState(false);
|
const [oversized, setOversized] = useState(false);
|
||||||
const [currentFile, setCurrentFile] = useState(null);
|
const [currentFile, setCurrentFile] = useState(null);
|
||||||
|
const [currentFileType, setCurrentFileType] = useState(null);
|
||||||
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
||||||
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
||||||
|
|
||||||
|
@ -58,11 +71,20 @@ function PublishFile(props: Props) {
|
||||||
const PROCESSING_MB_PER_SECOND = 0.5;
|
const PROCESSING_MB_PER_SECOND = 0.5;
|
||||||
const MINUTES_THRESHOLD = 30;
|
const MINUTES_THRESHOLD = 30;
|
||||||
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
|
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
|
||||||
|
const MARKDOWN_FILE_EXTENSIONS = ['txt', 'md', 'markdown'];
|
||||||
|
|
||||||
const sizeInMB = Number(size) / 1000000;
|
const sizeInMB = Number(size) / 1000000;
|
||||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||||
|
|
||||||
// clear warnings
|
// Reset filePath if publish mode changed
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === PUBLISH_MODES.STORY) {
|
||||||
|
if (currentFileType !== 'text/markdown') {
|
||||||
|
updatePublishForm({ filePath: '', name: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentFileType, mode, updatePublishForm]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filePath || filePath === '') {
|
if (!filePath || filePath === '') {
|
||||||
setCurrentFile('');
|
setCurrentFile('');
|
||||||
|
@ -82,7 +104,7 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
setOptimizeAvail(isOptimizeAvail);
|
setOptimizeAvail(isOptimizeAvail);
|
||||||
updatePublishForm({ optimize: finalOptimizeState });
|
updatePublishForm({ optimize: finalOptimizeState });
|
||||||
}, [currentFile, filePath, isVid, ffmpegAvail, userOptimize]);
|
}, [currentFile, filePath, isVid, ffmpegAvail, userOptimize, updatePublishForm]);
|
||||||
|
|
||||||
function updateFileInfo(duration, size, isvid) {
|
function updateFileInfo(duration, size, isvid) {
|
||||||
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
||||||
|
@ -201,6 +223,19 @@ function PublishFile(props: Props) {
|
||||||
const contentType = file.type && file.type.split('/');
|
const contentType = file.type && file.type.split('/');
|
||||||
const isVideo = contentType && contentType[0] === 'video';
|
const isVideo = contentType && contentType[0] === 'video';
|
||||||
const isMp4 = contentType && contentType[1] === 'mp4';
|
const isMp4 = contentType && contentType[1] === 'mp4';
|
||||||
|
|
||||||
|
let isMarkdownText = false;
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
isMarkdownText = contentType[0] === 'text';
|
||||||
|
setCurrentFileType(contentType);
|
||||||
|
} else if (file.name) {
|
||||||
|
// If user's machine is missign a valid content type registration
|
||||||
|
// for markdown content: text/markdown, file extension will be used instead
|
||||||
|
const extension = file.name.split('.').pop();
|
||||||
|
isMarkdownText = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
||||||
|
}
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
if (isMp4) {
|
if (isMp4) {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
|
@ -220,6 +255,22 @@ function PublishFile(props: Props) {
|
||||||
updateFileInfo(0, file.size, isVideo);
|
updateFileInfo(0, file.size, isVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMarkdownText) {
|
||||||
|
// Create reader
|
||||||
|
const reader = new FileReader();
|
||||||
|
// Handler for file reader
|
||||||
|
reader.addEventListener('load', event => {
|
||||||
|
const text = event.target.result;
|
||||||
|
updatePublishForm({ fileText: text });
|
||||||
|
setPublishMode(PUBLISH_MODES.STORY);
|
||||||
|
});
|
||||||
|
// Read file contents
|
||||||
|
reader.readAsText(file);
|
||||||
|
setCurrentFileType('text/markdown');
|
||||||
|
} else {
|
||||||
|
setPublishMode(PUBLISH_MODES.FILE);
|
||||||
|
}
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
// we only need to enforce file sizes on 'web'
|
// we only need to enforce file sizes on 'web'
|
||||||
if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT) {
|
if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT) {
|
||||||
|
@ -247,43 +298,62 @@ function PublishFile(props: Props) {
|
||||||
updatePublishForm(publishFormParams);
|
updatePublishForm(publishFormParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title;
|
let cardTitle;
|
||||||
if (publishing) {
|
if (publishing) {
|
||||||
title = (
|
cardTitle = (
|
||||||
<span>
|
<span>
|
||||||
{__('Uploading')}
|
{__('Uploading')}
|
||||||
<Spinner type={'small'} />
|
<Spinner type={'small'} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
title = isStillEditing ? __('Edit') : __('Upload');
|
cardTitle = isStillEditing ? __('Edit') : __('Upload');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPublishFile = mode === PUBLISH_MODES.FILE;
|
||||||
|
const isPublishStory = mode === PUBLISH_MODES.STORY;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
icon={ICONS.PUBLISH}
|
icon={ICONS.PUBLISH}
|
||||||
disabled={disabled || balance === 0}
|
disabled={disabled || balance === 0}
|
||||||
title={
|
title={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{title}{' '}
|
{cardTitle}{' '}
|
||||||
{inProgress && <Button button="close" label={__('Cancel')} icon={ICONS.REMOVE} onClick={clearPublish} />}
|
{inProgress && <Button button="close" label={__('Cancel')} icon={ICONS.REMOVE} onClick={clearPublish} />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
subtitle={isStillEditing && __('You are currently editing your upload.')}
|
subtitle={isStillEditing && __('You are currently editing your upload.')}
|
||||||
actions={
|
actions={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FileSelector disabled={disabled} currentPath={currentFile} onFileChosen={handleFileChange} />
|
|
||||||
{getMessage()}
|
|
||||||
{/* @if TARGET='app' */}
|
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="text"
|
||||||
checked={userOptimize}
|
name="content_title"
|
||||||
disabled={!optimizeAvail}
|
label={__('Title')}
|
||||||
onChange={() => setUserOptimize(!userOptimize)}
|
placeholder={__('Descriptive titles work best')}
|
||||||
label={__('Optimize and transcode video')}
|
disabled={disabled}
|
||||||
name="optimize"
|
value={title}
|
||||||
|
onChange={e => updatePublishForm({ title: e.target.value })}
|
||||||
/>
|
/>
|
||||||
{!ffmpegAvail && (
|
{isPublishFile && (
|
||||||
|
<FileSelector disabled={disabled} currentPath={currentFile} onFileChosen={handleFileChange} />
|
||||||
|
)}
|
||||||
|
{isPublishStory && (
|
||||||
|
<StoryEditor label={__('Story content')} uri={uri} disabled={disabled} setPrevFileText={setPrevFileText} />
|
||||||
|
)}
|
||||||
|
{isPublishFile && getMessage()}
|
||||||
|
{/* @if TARGET='app' */}
|
||||||
|
{isPublishFile && (
|
||||||
|
<FormField
|
||||||
|
type="checkbox"
|
||||||
|
checked={userOptimize}
|
||||||
|
disabled={!optimizeAvail}
|
||||||
|
onChange={() => setUserOptimize(!userOptimize)}
|
||||||
|
label={__('Optimize and transcode video')}
|
||||||
|
name="optimize"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isPublishFile && !ffmpegAvail && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
|
@ -294,7 +364,7 @@ function PublishFile(props: Props) {
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{Boolean(size) && ffmpegAvail && optimize && isVid && (
|
{isPublishFile && Boolean(size) && ffmpegAvail && optimize && isVid && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish.
|
On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish.
|
||||||
File upload is carried out in the background by that function.
|
File upload is carried out in the background by that function.
|
||||||
*/
|
*/
|
||||||
|
import fs from 'fs';
|
||||||
|
import { remote } from 'electron';
|
||||||
import { SITE_NAME } from 'config';
|
import { SITE_NAME } from 'config';
|
||||||
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
@ -15,7 +17,7 @@ import Button from 'component/button';
|
||||||
import SelectChannel from 'component/selectChannel';
|
import SelectChannel from 'component/selectChannel';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import TagsSelect from 'component/tagsSelect';
|
import TagsSelect from 'component/tagsSelect';
|
||||||
import PublishText from 'component/publishText';
|
import PublishDescription from 'component/publishDescription';
|
||||||
import PublishPrice from 'component/publishPrice';
|
import PublishPrice from 'component/publishPrice';
|
||||||
import PublishFile from 'component/publishFile';
|
import PublishFile from 'component/publishFile';
|
||||||
import PublishName from 'component/publishName';
|
import PublishName from 'component/publishName';
|
||||||
|
@ -24,12 +26,18 @@ import PublishFormErrors from 'component/publishFormErrors';
|
||||||
import SelectThumbnail from 'component/selectThumbnail';
|
import SelectThumbnail from 'component/selectThumbnail';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
|
|
||||||
|
const { dialog } = remote;
|
||||||
|
const currentWindow = remote.getCurrentWindow();
|
||||||
|
const MODES = Object.values(PUBLISH_MODES);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
tags: Array<Tag>,
|
tags: Array<Tag>,
|
||||||
publish: (?string) => void,
|
publish: (?string) => void,
|
||||||
filePath: ?string,
|
filePath: ?string,
|
||||||
|
fileText: ?string,
|
||||||
bid: ?number,
|
bid: ?number,
|
||||||
bidError: ?string,
|
bidError: ?string,
|
||||||
editingURI: ?string,
|
editingURI: ?string,
|
||||||
|
@ -73,6 +81,13 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishForm(props: Props) {
|
function PublishForm(props: Props) {
|
||||||
|
const [mode, setMode] = React.useState(PUBLISH_MODES.FILE);
|
||||||
|
const [autoSwitchMode, setAutoSwitchMode] = React.useState(true);
|
||||||
|
|
||||||
|
// Used to checl if the file has been modified by user
|
||||||
|
const [fileEdited, setFileEdited] = React.useState(false);
|
||||||
|
const [prevFileText, setPrevFileText] = React.useState('');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
thumbnail,
|
thumbnail,
|
||||||
name,
|
name,
|
||||||
|
@ -87,6 +102,7 @@ function PublishForm(props: Props) {
|
||||||
resetThumbnailStatus,
|
resetThumbnailStatus,
|
||||||
updatePublishForm,
|
updatePublishForm,
|
||||||
filePath,
|
filePath,
|
||||||
|
fileText,
|
||||||
publishing,
|
publishing,
|
||||||
clearPublish,
|
clearPublish,
|
||||||
isStillEditing,
|
isStillEditing,
|
||||||
|
@ -97,6 +113,7 @@ function PublishForm(props: Props) {
|
||||||
onChannelChange,
|
onChannelChange,
|
||||||
ytSignupPending,
|
ytSignupPending,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const TAGS_LIMIT = 5;
|
const TAGS_LIMIT = 5;
|
||||||
const formDisabled = (!filePath && !editingURI) || publishing;
|
const formDisabled = (!filePath && !editingURI) || publishing;
|
||||||
const isInProgress = filePath || editingURI || name || title;
|
const isInProgress = filePath || editingURI || name || title;
|
||||||
|
@ -129,6 +146,15 @@ function PublishForm(props: Props) {
|
||||||
}
|
}
|
||||||
}, [thumbnail, resetThumbnailStatus]);
|
}, [thumbnail, resetThumbnailStatus]);
|
||||||
|
|
||||||
|
// Check for content changes on the text editor
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fileEdited && fileText !== prevFileText && fileText !== '') {
|
||||||
|
setFileEdited(true);
|
||||||
|
} else if (fileEdited && fileText === prevFileText) {
|
||||||
|
setFileEdited(false);
|
||||||
|
}
|
||||||
|
}, [fileText, prevFileText, fileEdited]);
|
||||||
|
|
||||||
// Every time the channel or name changes, resolve the uris to find winning bid amounts
|
// Every time the channel or name changes, resolve the uris to find winning bid amounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If they are midway through a channel creation, treat it as anonymous until it completes
|
// If they are midway through a channel creation, treat it as anonymous until it completes
|
||||||
|
@ -161,14 +187,127 @@ function PublishForm(props: Props) {
|
||||||
updatePublishForm({ channel });
|
updatePublishForm({ channel });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSaveDialog() {
|
||||||
|
return dialog.showSaveDialog(currentWindow, {
|
||||||
|
filters: [{ name: 'Text', extensions: ['md', 'markdown', 'txt'] }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWebFile() {
|
||||||
|
if (fileText) {
|
||||||
|
const fileName = name || title || 'story';
|
||||||
|
return new File([fileText], `${fileName}.md`, { type: 'text/markdown' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFileChanges() {
|
||||||
|
let output = filePath;
|
||||||
|
if (!output || output === '') {
|
||||||
|
output = await showSaveDialog();
|
||||||
|
}
|
||||||
|
// User saved the file on a custom location
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
// Save file changes
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(output, fileText, (error, data) => {
|
||||||
|
// Handle error, cant save changes or create file
|
||||||
|
error ? reject(error) : resolve(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyStoryContent() {
|
||||||
|
const isEmpty = !fileText || fileText.length === 0 || fileText === '';
|
||||||
|
// TODO: Verify file size limit, and character size as well ?
|
||||||
|
return !isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePublish() {
|
||||||
|
// Publish story:
|
||||||
|
// If here is no file selected yet on desktop, show file dialog and let the
|
||||||
|
// user choose a file path. On web a new File is created
|
||||||
|
if (mode === PUBLISH_MODES.STORY) {
|
||||||
|
let outputFile = filePath;
|
||||||
|
// If user modified content on the text editor:
|
||||||
|
// Save changes and updat file path
|
||||||
|
if (fileEdited) {
|
||||||
|
// @if TARGET='app'
|
||||||
|
outputFile = await saveFileChanges();
|
||||||
|
// @endif
|
||||||
|
|
||||||
|
// @if TARGET='web'
|
||||||
|
outputFile = createWebFile();
|
||||||
|
// @endif
|
||||||
|
|
||||||
|
// New content stored locally and is not empty
|
||||||
|
if (outputFile) {
|
||||||
|
updatePublishForm({ filePath: outputFile });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify if story has a valid content and is not emoty
|
||||||
|
// On web file size limit will be verified as well
|
||||||
|
const verified = verifyStoryContent();
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
publish(outputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Publish file
|
||||||
|
if (mode === PUBLISH_MODES.FILE) {
|
||||||
|
publish(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePublishMode(name) {
|
||||||
|
setMode(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mode on editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoSwitchMode && editingURI && myClaimForUri) {
|
||||||
|
const { media_type: mediaType } = myClaimForUri.value.source;
|
||||||
|
// Change publish mode to "story" if editing content type is markdown
|
||||||
|
if (mediaType === 'text/markdown' && mode !== PUBLISH_MODES.STORY) {
|
||||||
|
setMode(PUBLISH_MODES.STORY);
|
||||||
|
// Prevent forced mode
|
||||||
|
setAutoSwitchMode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [autoSwitchMode, editingURI, myClaimForUri, mode, setMode, setAutoSwitchMode]);
|
||||||
|
|
||||||
|
// Editing claim uri
|
||||||
|
const uri = myClaimForUri ? myClaimForUri.permanent_url : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card-stack">
|
<div className="card-stack">
|
||||||
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
|
<div className="button-tab-group">
|
||||||
|
{MODES.map((name, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
icon={name}
|
||||||
|
label={name}
|
||||||
|
button="alt"
|
||||||
|
onClick={() => {
|
||||||
|
changePublishMode(name);
|
||||||
|
}}
|
||||||
|
className={classnames('button-toggle', { 'button-toggle--active': mode === name })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<PublishFile
|
||||||
|
uri={uri}
|
||||||
|
mode={mode}
|
||||||
|
disabled={disabled || publishing}
|
||||||
|
inProgress={isInProgress}
|
||||||
|
setPublishMode={setMode}
|
||||||
|
setPrevFileText={setPrevFileText}
|
||||||
|
/>
|
||||||
{!publishing && (
|
{!publishing && (
|
||||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||||
<PublishText disabled={formDisabled} />
|
{mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />}
|
||||||
<Card actions={<SelectThumbnail />} />
|
<Card actions={<SelectThumbnail />} />
|
||||||
|
|
||||||
<TagsSelect
|
<TagsSelect
|
||||||
suggestMature
|
suggestMature
|
||||||
disableAutoFocus
|
disableAutoFocus
|
||||||
|
@ -217,7 +356,7 @@ function PublishForm(props: Props) {
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
onClick={() => publish(filePath)}
|
onClick={handlePublish}
|
||||||
label={submitLabel}
|
label={submitLabel}
|
||||||
disabled={
|
disabled={
|
||||||
formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS || ytSignupPending
|
formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS || ytSignupPending
|
||||||
|
|
21
ui/component/storyEditor/index.js
Normal file
21
ui/component/storyEditor/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectIsStillEditing,
|
||||||
|
makeSelectPublishFormValue,
|
||||||
|
doUpdatePublishForm,
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import StoryEditor from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||||
|
fileText: makeSelectPublishFormValue('fileText')(state),
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(StoryEditor);
|
91
ui/component/storyEditor/view.jsx
Normal file
91
ui/component/storyEditor/view.jsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// @flow
|
||||||
|
import fs from 'fs';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { SIMPLE_SITE } from 'config';
|
||||||
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: ?string,
|
||||||
|
label: ?string,
|
||||||
|
disabled: ?boolean,
|
||||||
|
fileInfo: FileListItem,
|
||||||
|
filePath: string | WebFile,
|
||||||
|
fileText: ?string,
|
||||||
|
isStillEditing: boolean,
|
||||||
|
setPrevFileText: string => void,
|
||||||
|
updatePublishForm: ({}) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function StoryEditor(props: Props) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
label,
|
||||||
|
disabled,
|
||||||
|
fileInfo,
|
||||||
|
filePath,
|
||||||
|
fileText,
|
||||||
|
isStillEditing,
|
||||||
|
setPrevFileText,
|
||||||
|
updatePublishForm,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('publish-form-story-mode', false);
|
||||||
|
|
||||||
|
function toggleMarkdown() {
|
||||||
|
setAdvancedEditor(!advancedEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// @if TARGET='app'
|
||||||
|
function readFile(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.readFile(path, 'utf8', (error, data) => {
|
||||||
|
error ? reject(error) : resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEditorText(path) {
|
||||||
|
const text = await readFile(path);
|
||||||
|
if (text) {
|
||||||
|
// Store original content
|
||||||
|
setPrevFileText(text);
|
||||||
|
// Update text editor form
|
||||||
|
updatePublishForm({ fileText: text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEditingFile = isStillEditing && uri && fileInfo;
|
||||||
|
|
||||||
|
if (isEditingFile) {
|
||||||
|
const { mime_type: mimeType, download_path: downloadPath } = fileInfo;
|
||||||
|
|
||||||
|
// Editing same file (previously published)
|
||||||
|
// User can use a different file to replace the content
|
||||||
|
if (!filePath && mimeType === 'text/markdown') {
|
||||||
|
updateEditorText(downloadPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @endif
|
||||||
|
}, [uri, isStillEditing, filePath, fileInfo, setPrevFileText, updatePublishForm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||||
|
name="content_story"
|
||||||
|
label={label}
|
||||||
|
placeholder={__('My content for this story...')}
|
||||||
|
value={fileText}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={value => updatePublishForm({ fileText: advancedEditor ? value : value.target.value })}
|
||||||
|
quickActionLabel={advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||||
|
quickActionHandler={toggleMarkdown}
|
||||||
|
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StoryEditor;
|
2
ui/constants/publish_types.js
Normal file
2
ui/constants/publish_types.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const FILE = 'File';
|
||||||
|
export const STORY = 'Story';
|
|
@ -251,7 +251,12 @@ svg + .button__label,
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-tab-group {
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group,
|
||||||
|
.button-tab-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.button:first-child:not(:only-child) {
|
.button:first-child:not(:only-child) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue