Merge pull request #595 from lbryio/update-claims
allow claims to be updated and abandoned
This commit is contained in:
commit
dde6956283
60 changed files with 1080 additions and 280 deletions
|
@ -14,6 +14,19 @@ export function clearFile () {
|
|||
};
|
||||
}
|
||||
|
||||
export function setUpdateTrue () {
|
||||
return {
|
||||
type: actions.SET_UPDATE_TRUE,
|
||||
};
|
||||
}
|
||||
|
||||
export function setHasChanged (status) {
|
||||
return {
|
||||
type: actions.SET_HAS_CHANGED,
|
||||
data: status,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateMetadata (name, value) {
|
||||
return {
|
||||
type: actions.METADATA_UPDATE,
|
||||
|
@ -31,6 +44,13 @@ export function updateClaim (value) {
|
|||
};
|
||||
};
|
||||
|
||||
export function abandonClaim (data) {
|
||||
return {
|
||||
type: actions.ABANDON_CLAIM,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export function setPublishInChannel (channel) {
|
||||
return {
|
||||
type: actions.SET_PUBLISH_IN_CHANNEL,
|
||||
|
|
|
@ -105,6 +105,13 @@ export function updateAssetViewsInList (id, claimId, claimViews) {
|
|||
};
|
||||
}
|
||||
|
||||
export function removeAsset (data) {
|
||||
return {
|
||||
type: actions.ASSET_REMOVE,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
// channel actions
|
||||
|
||||
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {
|
||||
|
@ -129,7 +136,7 @@ export function onUpdateChannelClaims (channelKey, name, longId, page) {
|
|||
|
||||
export function updateChannelClaims (channelListId, claimsData) {
|
||||
return {
|
||||
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCESS,
|
||||
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED,
|
||||
data: {channelListId, claimsData},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -42,3 +42,15 @@ export function getClaimViews (claimId) {
|
|||
const url = `/api/claim/views/${claimId}`;
|
||||
return Request(url);
|
||||
}
|
||||
|
||||
export function doAbandonClaim (claimId) {
|
||||
const params = {
|
||||
method : 'POST',
|
||||
body : JSON.stringify({claimId}),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
credentials: 'include',
|
||||
};
|
||||
return Request('/api/claim/abandon', params);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import ContentPageWrapper from '@pages/ContentPageWrapper';
|
|||
import FourOhFourPage from '@pages/FourOhFourPage';
|
||||
import MultisitePage from '@pages/MultisitePage';
|
||||
import PopularPage from '@pages/PopularPage';
|
||||
import EditPage from '@pages/EditPage';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
|
@ -21,6 +22,7 @@ const App = () => {
|
|||
<Route exact path='/login' component={LoginPage} />
|
||||
<Route exact path='/multisite' component={MultisitePage} />
|
||||
<Route exact path='/popular' component={PopularPage} />
|
||||
<Route exact path='/edit/:identifier/:claim' component={EditPage} />
|
||||
<Route exact path='/:identifier/:claim' component={ContentPageWrapper} />
|
||||
<Route exact path='/:claim' component={ContentPageWrapper} />
|
||||
<Route component={FourOhFourPage} />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {buffers, END, eventChannel} from 'redux-saga';
|
||||
|
||||
export const makePublishRequestChannel = (fd) => {
|
||||
export const makePublishRequestChannel = (fd, isUpdate) => {
|
||||
return eventChannel(emitter => {
|
||||
const uri = '/api/claim/publish';
|
||||
const uri = `/api/claim/${isUpdate ? 'update' : 'publish'}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
// add event listeners
|
||||
const onLoadStart = () => {
|
||||
|
|
|
@ -2,11 +2,14 @@ import React from 'react';
|
|||
import FormFeedbackDisplay from '@components/FormFeedbackDisplay';
|
||||
import Row from '@components/Row';
|
||||
|
||||
const DropzoneInstructionsDisplay = ({fileError}) => {
|
||||
const DropzoneInstructionsDisplay = ({fileError, message}) => {
|
||||
if (!message) {
|
||||
message = 'Drag & drop image or video here to publish';
|
||||
}
|
||||
return (
|
||||
<div className={'dropzone-instructions-display'}>
|
||||
<Row>
|
||||
<p className={'text--large'}>Drag & drop image or video here to publish</p>
|
||||
<p className={'text--large'}>{message}</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<p className={'text--small'}>OR</p>
|
||||
|
|
|
@ -10,7 +10,12 @@ class PublishPreview extends React.Component {
|
|||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.setPreviewImageSource(this.props.file);
|
||||
const { isUpdate, sourceUrl, file } = this.props;
|
||||
if (isUpdate && sourceUrl) {
|
||||
this.setState({ imgSource: sourceUrl });
|
||||
} else {
|
||||
this.setPreviewImageSource(file);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.file !== this.props.file) {
|
||||
|
@ -54,8 +59,10 @@ class PublishPreview extends React.Component {
|
|||
|
||||
PublishPreview.propTypes = {
|
||||
dimPreview: PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired,
|
||||
file : PropTypes.object,
|
||||
thumbnail : PropTypes.object,
|
||||
isUpdate : PropTypes.bool,
|
||||
sourceUrl : PropTypes.string,
|
||||
};
|
||||
|
||||
export default PublishPreview;
|
||||
|
|
|
@ -16,7 +16,7 @@ const PublishLicenseInput = ({ handleSelect }) => {
|
|||
className='select select--primary'
|
||||
onChange={handleSelect}
|
||||
>
|
||||
<option value=' '>Unspecified</option>
|
||||
<option value=''>Unspecified</option>
|
||||
<option value='Public Domain'>Public Domain</option>
|
||||
<option value='Creative Commons'>Creative Commons</option>
|
||||
</select>
|
||||
|
|
|
@ -7,9 +7,11 @@ import Row from '@components/Row';
|
|||
|
||||
class PublishPreview extends React.Component {
|
||||
render () {
|
||||
const { isUpdate, uri } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
{isUpdate && uri && (<p className='text--extra-small'>{`Editing ${uri}`}</p>)}
|
||||
<PublishTitleInput />
|
||||
</Row>
|
||||
<HorizontalSplit
|
||||
|
|
1
client/src/constants/confirmation_messages.js
Normal file
1
client/src/constants/confirmation_messages.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const SAVE = 'Everything not saved will be lost. Are you sure you want to leave this page?';
|
|
@ -10,3 +10,6 @@ export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
|
|||
export const THUMBNAIL_NEW = 'THUMBNAIL_NEW';
|
||||
export const PUBLISH_START = 'PUBLISH_START';
|
||||
export const CLAIM_AVAILABILITY = 'CLAIM_AVAILABILITY';
|
||||
export const SET_UPDATE_TRUE = 'SET_UPDATE_TRUE';
|
||||
export const ABANDON_CLAIM = 'ABANDON_CLAIM';
|
||||
export const SET_HAS_CHANGED = 'SET_HAS_CHANGED';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const LOAD_START = 'LOAD_START';
|
||||
export const LOADING = 'LOADING';
|
||||
export const PUBLISHING = 'PUBLISHING';
|
||||
export const SUCCESS = 'SUCCESS';
|
||||
export const SUCCEEDED = 'SUCCEEDED';
|
||||
export const FAILED = 'FAILED';
|
||||
export const ABANDONING = 'ABANDONING';
|
||||
|
|
|
@ -11,12 +11,14 @@ export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
|
|||
// asset actions
|
||||
export const ASSET_ADD = 'ASSET_ADD';
|
||||
export const ASSET_VIEWS_UPDATE = 'ASSET_VIEWS_UPDATE';
|
||||
export const ASSET_UPDATE_CLAIMDATA = 'ASSET_UPDATE_CLAIMDATA';
|
||||
export const ASSET_REMOVE = 'ASSET_REMOVE';
|
||||
|
||||
// channel actions
|
||||
export const CHANNEL_ADD = 'CHANNEL_ADD';
|
||||
|
||||
export const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC';
|
||||
export const CHANNEL_CLAIMS_UPDATE_SUCCESS = 'CHANNEL_CLAIMS_UPDATE_SUCCESS';
|
||||
export const CHANNEL_CLAIMS_UPDATE_SUCCEEDED = 'CHANNEL_CLAIMS_UPDATE_SUCCEEDED';
|
||||
|
||||
// asset/file display actions
|
||||
export const FILE_REQUESTED = 'FILE_REQUESTED';
|
||||
|
|
|
@ -3,7 +3,8 @@ import View from './view';
|
|||
import { fileRequested } from '../../actions/show';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
const mapStateToProps = (props) => {
|
||||
const {show} = props;
|
||||
// select error and status
|
||||
const error = show.displayAsset.error;
|
||||
const status = show.displayAsset.status;
|
||||
|
|
|
@ -2,6 +2,42 @@ import React from 'react';
|
|||
import Row from '@components/Row';
|
||||
import ProgressBar from '@components/ProgressBar';
|
||||
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';
|
||||
import createCanonicalLink from '../../../../utils/createCanonicalLink';
|
||||
|
||||
class AvailableContent extends React.Component {
|
||||
render () {
|
||||
const {contentType, sourceUrl, name, thumbnail} = this.props;
|
||||
switch (contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img
|
||||
className='asset-image'
|
||||
src={sourceUrl}
|
||||
alt={name}
|
||||
/>
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video
|
||||
className='asset-video'
|
||||
controls poster={thumbnail}
|
||||
>
|
||||
<source
|
||||
src={sourceUrl}
|
||||
/>
|
||||
<p>Your browser does not support the <code>video</code> element.</p>
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>Unsupported content type</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AssetDisplay extends React.Component {
|
||||
componentDidMount () {
|
||||
|
@ -9,8 +45,15 @@ class AssetDisplay extends React.Component {
|
|||
this.props.onFileRequest(name, claimId);
|
||||
}
|
||||
render () {
|
||||
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
|
||||
const sourceUrl = `/${claimId}/${name}.${fileExt}`;
|
||||
const { status, error, asset } = this.props;
|
||||
const { name, claimData: { claimId, contentType, thumbnail, outpoint } } = asset;
|
||||
// the outpoint is added to force the browser to re-download the asset after an update
|
||||
// issue: https://github.com/lbryio/spee.ch/issues/607
|
||||
let fileExt;
|
||||
if (typeof contentType === 'string') {
|
||||
fileExt = contentType.split('/')[1] || 'jpg';
|
||||
}
|
||||
const sourceUrl = `${createCanonicalLink({ asset: asset.claimData })}.${fileExt}?${outpoint}`;
|
||||
return (
|
||||
<div className={'asset-display'}>
|
||||
{(status === LOCAL_CHECK) &&
|
||||
|
@ -36,37 +79,12 @@ class AssetDisplay extends React.Component {
|
|||
</div>
|
||||
}
|
||||
{(status === AVAILABLE) &&
|
||||
(() => {
|
||||
switch (contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img
|
||||
className='asset-image'
|
||||
src={sourceUrl}
|
||||
alt={name}
|
||||
/>
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video
|
||||
className='asset-video'
|
||||
controls poster={thumbnail}
|
||||
>
|
||||
<source
|
||||
src={sourceUrl}
|
||||
/>
|
||||
<p>Your browser does not support the <code>video</code> element.</p>
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>Unsupported content type</p>
|
||||
);
|
||||
}
|
||||
})()
|
||||
<AvailableContent
|
||||
contentType={contentType}
|
||||
sourceUrl={sourceUrl}
|
||||
name={name}
|
||||
thumbnail={thumbnail}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,12 +2,20 @@ import { connect } from 'react-redux';
|
|||
import View from './view';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
const mapStateToProps = (props) => {
|
||||
const {show} = props;
|
||||
// select asset
|
||||
const asset = selectAsset(show);
|
||||
const editable = Boolean(
|
||||
asset &&
|
||||
asset.claimData &&
|
||||
asset.claimData.channelName &&
|
||||
props.channel.loggedInChannel.name === asset.claimData.channelName
|
||||
);
|
||||
// return props
|
||||
return {
|
||||
asset,
|
||||
editable,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -13,10 +13,11 @@ import createCanonicalLink from '../../../../utils/createCanonicalLink';
|
|||
|
||||
class AssetInfo extends React.Component {
|
||||
render () {
|
||||
const { asset } = this.props;
|
||||
const { claimViews, claimData: { channelName, channelShortId, description, name, fileExt, contentType, thumbnail, host } } = asset;
|
||||
const { editable, asset } = this.props;
|
||||
const { claimViews, claimData } = asset;
|
||||
const { channelName, claimId, channelShortId, description, name, fileExt, contentType, host } = claimData;
|
||||
|
||||
const canonicalUrl = createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }});
|
||||
const canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId: asset.shortId }});
|
||||
const assetCanonicalUrl = `${host}${canonicalUrl}`;
|
||||
|
||||
let channelCanonicalUrl;
|
||||
|
@ -29,6 +30,15 @@ class AssetInfo extends React.Component {
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
{editable && (
|
||||
<Row>
|
||||
<RowLabeled
|
||||
label={<Label value={'Edit:'} />}
|
||||
content={<Link to={`/edit${canonicalUrl}`}>{name}</Link>}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{channelName && (
|
||||
<Row>
|
||||
<RowLabeled
|
||||
|
|
|
@ -2,8 +2,8 @@ import { connect } from 'react-redux';
|
|||
import View from './view';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
const { claimData: { title } } = selectAsset(show);
|
||||
const mapStateToProps = (props) => {
|
||||
const { claimData: { title } } = selectAsset(props.show);
|
||||
return {
|
||||
title,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Row from '@components/Row';
|
||||
|
||||
const AssetTitle = ({ title }) => {
|
||||
|
|
|
@ -36,7 +36,7 @@ class ChannelClaimsDisplay extends React.Component {
|
|||
<AssetPreview
|
||||
defaultThumbnail={defaultThumbnail}
|
||||
claimData={claim}
|
||||
key={`${claim.name}-${claim.id}`}
|
||||
key={claim.claimId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectFile, updateError, clearFile } from '../../actions/publish';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
import View from './view';
|
||||
import siteConfig from '@config/siteConfig.json';
|
||||
import createCanonicalLink from '../../../../utils/createCanonicalLink';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
file : publish.file,
|
||||
thumbnail: publish.thumbnail,
|
||||
fileError: publish.error.file,
|
||||
};
|
||||
const { assetDefaults: { thumbnail: defaultThumbnail } } = siteConfig;
|
||||
|
||||
const mapStateToProps = ({ show, publish: { file, thumbnail, fileError, isUpdate } }) => {
|
||||
const obj = { file, thumbnail, fileError, isUpdate };
|
||||
let asset, name, claimId, fileExt, outpoint, sourceUrl;
|
||||
if (isUpdate) {
|
||||
asset = selectAsset(show);
|
||||
const { claimData } = asset;
|
||||
if (asset) {
|
||||
obj.fileExt = claimData.contentType.split('/')[1];
|
||||
if (obj.fileExt === 'mp4') {
|
||||
obj.sourceUrl = claimData.thumbnail ? claimData.thumbnail : defaultThumbnail;
|
||||
} else {
|
||||
({fileExt, outpoint} = claimData);
|
||||
obj.sourceUrl = `${createCanonicalLink({ asset: claimData })}.${fileExt}?${outpoint}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
|
|
|
@ -81,53 +81,70 @@ class Dropzone extends React.Component {
|
|||
}
|
||||
}
|
||||
render () {
|
||||
const { dragOver, mouseOver, dimPreview } = this.state;
|
||||
const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;
|
||||
return (
|
||||
<div className='dropzone-wrapper'>
|
||||
<form>
|
||||
<input
|
||||
className='input-file'
|
||||
type='file'
|
||||
id='file_input'
|
||||
name='file_input'
|
||||
accept='video/*,image/*'
|
||||
onChange={this.handleFileInput}
|
||||
encType='multipart/form-data'
|
||||
/>
|
||||
</form>
|
||||
<div
|
||||
className={'dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')}
|
||||
onDrop={this.handleDrop}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnd={this.handleDragEnd}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onClick={this.handleClick}>
|
||||
{this.props.file ? (
|
||||
<div className={'dropzone-preview-wrapper'}>
|
||||
<DropzonePreviewImage
|
||||
dimPreview={this.state.dimPreview}
|
||||
file={this.props.file}
|
||||
thumbnail={this.props.thumbnail}
|
||||
<div>
|
||||
{isUpdate && fileExt === 'mp4' ? (
|
||||
<p>Video updates are currently disabled. This feature will be available soon. You can edit metadata.</p>
|
||||
) : (
|
||||
<div className='dropzone-wrapper'>
|
||||
<form>
|
||||
<input
|
||||
className='input-file'
|
||||
type='file'
|
||||
id='file_input'
|
||||
name='file_input'
|
||||
accept='video/*,image/*'
|
||||
onChange={this.handleFileInput}
|
||||
encType='multipart/form-data'
|
||||
/>
|
||||
<div className={'dropzone-preview-overlay'}>
|
||||
{ this.state.dragOver ? <DropzoneDropItDisplay /> : null }
|
||||
{ this.state.mouseOver ? (
|
||||
</form>
|
||||
<div
|
||||
className={'dropzone' + (dragOver ? ' dropzone--drag-over' : '')}
|
||||
onDrop={this.handleDrop}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnd={this.handleDragEnd}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onClick={this.handleClick}>
|
||||
{file || isUpdate ? (
|
||||
<div className={'dropzone-preview-wrapper'}>
|
||||
{file ? (
|
||||
<DropzonePreviewImage
|
||||
dimPreview={dimPreview}
|
||||
file={file}
|
||||
thumbnail={thumbnail}
|
||||
/>
|
||||
) : (
|
||||
<DropzonePreviewImage
|
||||
dimPreview
|
||||
isUpdate
|
||||
sourceUrl={sourceUrl}
|
||||
/>
|
||||
)}
|
||||
<div className={'dropzone-preview-overlay'}>
|
||||
{ dragOver ? <DropzoneDropItDisplay /> : null }
|
||||
{ mouseOver ? (
|
||||
<DropzoneInstructionsDisplay
|
||||
fileError={fileError}
|
||||
message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}
|
||||
/>
|
||||
) : null }
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
dragOver ? <DropzoneDropItDisplay /> : (
|
||||
<DropzoneInstructionsDisplay
|
||||
fileError={this.props.fileError}
|
||||
fileError={fileError}
|
||||
/>
|
||||
) : null }
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
this.state.dragOver ? <DropzoneDropItDisplay /> : (
|
||||
<DropzoneInstructionsDisplay
|
||||
fileError={this.props.fileError}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { clearFile, startPublish } from '../../actions/publish';
|
||||
import { clearFile, startPublish, abandonClaim } from '../../actions/publish';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ channel, publish }) => {
|
||||
const mapStateToProps = ({ show, publish }) => {
|
||||
return {
|
||||
file: publish.file,
|
||||
isUpdate: publish.isUpdate,
|
||||
hasChanged: publish.hasChanged,
|
||||
asset: selectAsset(show),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearFile,
|
||||
startPublish,
|
||||
abandonClaim,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,35 +1,76 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {Link, withRouter} from 'react-router-dom';
|
||||
import PublishUrlInput from '@containers/PublishUrlInput';
|
||||
import PublishThumbnailInput from '@containers/PublishThumbnailInput';
|
||||
import PublishMetadataInputs from '@containers/PublishMetadataInputs';
|
||||
import ChannelSelect from '@containers/ChannelSelect';
|
||||
import Row from '@components/Row';
|
||||
import Label from '@components/Label';
|
||||
import RowLabeled from '@components/RowLabeled';
|
||||
import ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';
|
||||
import ButtonTertiary from '@components/ButtonTertiary';
|
||||
import ButtonSecondary from '@components/ButtonSecondary';
|
||||
import SpaceAround from '@components/SpaceAround';
|
||||
import PublishFinePrint from '@components/PublishFinePrint';
|
||||
import { SAVE } from '../../constants/confirmation_messages';
|
||||
|
||||
class PublishDetails extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onPublishSubmit = this.onPublishSubmit.bind(this);
|
||||
this.abandonClaim = this.abandonClaim.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
}
|
||||
onPublishSubmit () {
|
||||
this.props.startPublish(this.props.history);
|
||||
}
|
||||
abandonClaim () {
|
||||
const {asset, history} = this.props;
|
||||
if (asset) {
|
||||
const {claimData} = asset;
|
||||
this.props.abandonClaim({claimData, history});
|
||||
}
|
||||
}
|
||||
onCancel () {
|
||||
const { isUpdate, hasChanged, clearFile, history } = this.props;
|
||||
if (isUpdate || !hasChanged) {
|
||||
history.push('/');
|
||||
} else {
|
||||
if (confirm(SAVE)) {
|
||||
clearFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const {file, isUpdate, asset} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<PublishUrlInput />
|
||||
</Row>
|
||||
{isUpdate ? (asset && (
|
||||
<Row>
|
||||
<RowLabeled
|
||||
label={
|
||||
<Label value={'Channel:'} />
|
||||
}
|
||||
content={
|
||||
<span className='text'>
|
||||
{asset.claimData.channelName}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
)) : (
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<PublishUrlInput />
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<ChannelSelect />
|
||||
</Row>
|
||||
<Row>
|
||||
<ChannelSelect />
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{ this.props.file.type === 'video/mp4' && (
|
||||
{ file && file.type === 'video/mp4' && (
|
||||
<Row>
|
||||
<PublishThumbnailInput />
|
||||
</Row>
|
||||
|
@ -41,16 +82,27 @@ class PublishDetails extends React.Component {
|
|||
|
||||
<Row>
|
||||
<ButtonPrimaryJumbo
|
||||
value={'Publish'}
|
||||
value={isUpdate ? 'Update' : 'Publish'}
|
||||
onClickHandler={this.onPublishSubmit}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{isUpdate && (
|
||||
<Row>
|
||||
<SpaceAround>
|
||||
<ButtonSecondary
|
||||
value={'Abandon Claim'}
|
||||
onClickHandler={this.abandonClaim}
|
||||
/>
|
||||
</SpaceAround>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<SpaceAround>
|
||||
<ButtonTertiary
|
||||
value={'Cancel'}
|
||||
onClickHandler={this.props.clearFile}
|
||||
onClickHandler={this.onCancel}
|
||||
/>
|
||||
</SpaceAround>
|
||||
</Row>
|
||||
|
|
|
@ -8,6 +8,7 @@ const mapStateToProps = ({ publish }) => {
|
|||
description : publish.metadata.description,
|
||||
license : publish.metadata.license,
|
||||
nsfw : publish.metadata.nsfw,
|
||||
isUpdate : publish.isUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -26,27 +26,30 @@ class PublishMetadataInputs extends React.Component {
|
|||
this.props.onMetadataChange(name, selectedOption);
|
||||
}
|
||||
render () {
|
||||
const { showMetadataInputs, description, isUpdate, nsfw } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{this.props.showMetadataInputs && (
|
||||
{(showMetadataInputs || isUpdate) && (
|
||||
<div>
|
||||
<PublishDescriptionInput
|
||||
description={this.props.description}
|
||||
description={description}
|
||||
handleInput={this.handleInput}
|
||||
/>
|
||||
<PublishLicenseInput
|
||||
handleSelect={this.handleSelect}
|
||||
/>
|
||||
<PublishNsfwInput
|
||||
nsfw={this.props.nsfw}
|
||||
nsfw={nsfw}
|
||||
handleInput={this.handleInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ButtonSecondary
|
||||
value={this.props.showMetadataInputs ? 'less' : 'more'}
|
||||
onClickHandler={this.toggleShowInputs}
|
||||
/>
|
||||
{!isUpdate && (
|
||||
<ButtonSecondary
|
||||
value={showMetadataInputs ? 'less' : 'more'}
|
||||
onClickHandler={this.toggleShowInputs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class PublishStatus extends React.Component {
|
|||
{status === publishStates.LOAD_START &&
|
||||
<div className={'status'}>
|
||||
<Row>
|
||||
<p>le is loading to server</p>
|
||||
<p>File is loading to server</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<p className={'text--secondary'}>0%</p>
|
||||
|
@ -42,7 +42,7 @@ class PublishStatus extends React.Component {
|
|||
</Row>
|
||||
</div>
|
||||
}
|
||||
{status === publishStates.SUCCESS &&
|
||||
{status === publishStates.SUCCEEDED &&
|
||||
<div className={'status'}>
|
||||
<Row>
|
||||
<p>Your publish is complete! You are being redirected to it now.</p>
|
||||
|
@ -71,6 +71,13 @@ class PublishStatus extends React.Component {
|
|||
</Row>
|
||||
</div>
|
||||
}
|
||||
{status === publishStates.ABANDONING &&
|
||||
<div className={'status'}>
|
||||
<Row>
|
||||
<p>Your claim is being abandoned.</p>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
import {connect} from 'react-redux';
|
||||
import View from './view';
|
||||
import {selectAsset} from "../../selectors/show";
|
||||
import {buildURI} from "../../utils/buildURI";
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
const mapStateToProps = props => {
|
||||
const { show, publish } = props;
|
||||
const asset = selectAsset(show);
|
||||
let uri;
|
||||
if (asset) {
|
||||
uri = `lbry://${buildURI(asset)}`;
|
||||
}
|
||||
return {
|
||||
disabled: publish.disabled,
|
||||
file : publish.file,
|
||||
status : publish.status.status,
|
||||
file: publish.file,
|
||||
status: publish.status.status,
|
||||
isUpdate: publish.isUpdate,
|
||||
hasChanged: publish.hasChanged,
|
||||
uri,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,34 @@
|
|||
import React from 'react';
|
||||
import { withRouter, Prompt } from 'react-router';
|
||||
import Dropzone from '@containers/Dropzone';
|
||||
import PublishPreview from '@components/PublishPreview';
|
||||
import PublishStatus from '@containers/PublishStatus';
|
||||
import PublishDisabledMessage from '@containers/PublishDisabledMessage';
|
||||
import { SAVE } from '../../constants/confirmation_messages';
|
||||
|
||||
class PublishTool extends React.Component {
|
||||
render () {
|
||||
if (this.props.disabled) {
|
||||
const {disabled, file, isUpdate, hasChanged, uri, status, location: currentLocation} = this.props;
|
||||
if (disabled) {
|
||||
return (
|
||||
<PublishDisabledMessage />
|
||||
);
|
||||
} else {
|
||||
if (this.props.file) {
|
||||
if (this.props.status) {
|
||||
if (file || isUpdate) {
|
||||
if (status) {
|
||||
return (
|
||||
<PublishStatus />
|
||||
);
|
||||
} else {
|
||||
return <PublishPreview />;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Prompt
|
||||
when={hasChanged}
|
||||
message={(location) => location.pathname === currentLocation.pathname ? false : SAVE}
|
||||
/>
|
||||
<PublishPreview isUpdate={isUpdate} uri={uri} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Dropzone />;
|
||||
|
@ -25,4 +36,4 @@ class PublishTool extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
export default PublishTool;
|
||||
export default withRouter(PublishTool);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import PageLayout from '@components/PageLayout';
|
||||
import HorizontalSplit from '@components/HorizontalSplit';
|
||||
import AboutSpeechOverview from '@components/AboutSpeechOverview';
|
||||
|
@ -20,4 +21,4 @@ class AboutPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default AboutPage;
|
||||
export default withRouter(AboutPage);
|
||||
|
|
24
client/src/pages/EditPage/index.js
Normal file
24
client/src/pages/EditPage/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { setUpdateTrue, setHasChanged, updateMetadata, clearFile } from '../../actions/publish';
|
||||
import { onHandleShowPageUri } from '../../actions/show';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = (props) => {
|
||||
const { show } = props;
|
||||
return {
|
||||
asset : selectAsset(show),
|
||||
myChannel: props.channel.loggedInChannel.name,
|
||||
isUpdate : props.publish.isUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateMetadata,
|
||||
onHandleShowPageUri,
|
||||
setUpdateTrue,
|
||||
setHasChanged,
|
||||
clearFile,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
43
client/src/pages/EditPage/view.jsx
Normal file
43
client/src/pages/EditPage/view.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import PageLayout from '@components/PageLayout';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import PublishTool from '@containers/PublishTool';
|
||||
|
||||
class EditPage extends React.Component {
|
||||
componentDidMount () {
|
||||
const {asset, match, onHandleShowPageUri, setUpdateTrue, setHasChanged, updateMetadata} = this.props;
|
||||
onHandleShowPageUri(match.params);
|
||||
setUpdateTrue();
|
||||
if (asset) {
|
||||
['title', 'description', 'license', 'nsfw'].forEach(meta => updateMetadata(meta, asset.claimData[meta]));
|
||||
}
|
||||
setHasChanged(false);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.clearFile();
|
||||
}
|
||||
render () {
|
||||
const { myChannel, asset } = this.props;
|
||||
// redirect if user does not own this claim
|
||||
if (
|
||||
!myChannel || (
|
||||
asset &&
|
||||
asset.claimsData &&
|
||||
asset.claimsData.channelName &&
|
||||
asset.claimsData.channelName !== myChannel
|
||||
)
|
||||
) {
|
||||
return (<Redirect to={'/'} />);
|
||||
}
|
||||
return (
|
||||
<PageLayout
|
||||
pageTitle={'Edit claim'}
|
||||
pageUri={'edit'}
|
||||
>
|
||||
<PublishTool />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default EditPage;
|
|
@ -1,17 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { onHandleShowHomepage } from '../../actions/show';
|
||||
import { clearFile } from '../../actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show, site, channel }) => {
|
||||
const mapStateToProps = ({ show, site, channel, publish }) => {
|
||||
return {
|
||||
error : show.request.error,
|
||||
requestType: show.request.type,
|
||||
homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,
|
||||
isUpdate : publish.isUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onHandleShowHomepage,
|
||||
clearFile,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
|
@ -4,16 +4,9 @@ import PublishTool from '@containers/PublishTool';
|
|||
import ContentPageWrapper from '@pages/ContentPageWrapper';
|
||||
|
||||
class HomePage extends React.Component {
|
||||
componentDidMount () {
|
||||
this.props.onHandleShowHomepage(this.props.match.params);
|
||||
componentWillUnmount () {
|
||||
this.props.clearFile();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.match.params !== this.props.match.params) {
|
||||
this.props.onHandleShowHomepage(nextProps.match.params);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { homeChannel } = this.props;
|
||||
return homeChannel ? (
|
||||
|
|
|
@ -1,20 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectAsset } from '../../selectors/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select request info
|
||||
const requestId = show.request.id;
|
||||
// select asset info
|
||||
let asset;
|
||||
const request = show.requestList[requestId] || null;
|
||||
const assetList = show.assetList;
|
||||
if (request && assetList) {
|
||||
const assetKey = request.key; // note: just store this in the request
|
||||
asset = assetList[assetKey] || null;
|
||||
};
|
||||
// return props
|
||||
return {
|
||||
asset,
|
||||
asset: selectAsset(show),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import PageLayout from '@components/PageLayout';
|
||||
|
||||
import HorizontalSplit from '@components/HorizontalSplit';
|
||||
import AssetTitle from '@containers/AssetTitle';
|
||||
import AssetDisplay from '@containers/AssetDisplay';
|
||||
|
|
|
@ -41,6 +41,8 @@ const initialState = {
|
|||
license : '',
|
||||
nsfw : false,
|
||||
},
|
||||
isUpdate: false,
|
||||
hasChanged: false,
|
||||
thumbnail: null,
|
||||
thumbnailChannel,
|
||||
thumbnailChannelId,
|
||||
|
@ -49,8 +51,9 @@ const initialState = {
|
|||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FILE_SELECTED:
|
||||
return Object.assign({}, initialState, { // note: clears to initial state
|
||||
return Object.assign({}, state.isUpdate ? state : initialState, { // note: clears to initial state
|
||||
file: action.data,
|
||||
hasChanged: true,
|
||||
});
|
||||
case actions.FILE_CLEAR:
|
||||
return initialState;
|
||||
|
@ -59,14 +62,17 @@ export default function (state = initialState, action) {
|
|||
metadata: Object.assign({}, state.metadata, {
|
||||
[action.data.name]: action.data.value,
|
||||
}),
|
||||
hasChanged: true,
|
||||
});
|
||||
case actions.CLAIM_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
claim: action.data,
|
||||
hasChanged: true,
|
||||
});
|
||||
case actions.SET_PUBLISH_IN_CHANNEL:
|
||||
return Object.assign({}, state, {
|
||||
publishInChannel: action.channel,
|
||||
hasChanged: true,
|
||||
});
|
||||
case actions.PUBLISH_STATUS_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
|
@ -83,13 +89,26 @@ export default function (state = initialState, action) {
|
|||
selectedChannel: action.data,
|
||||
});
|
||||
case actions.TOGGLE_METADATA_INPUTS:
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
showMetadataInputs: action.data,
|
||||
});
|
||||
};
|
||||
case actions.THUMBNAIL_NEW:
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
thumbnail: action.data,
|
||||
});
|
||||
hasChanged: true,
|
||||
};
|
||||
case actions.SET_UPDATE_TRUE:
|
||||
return {
|
||||
...state,
|
||||
isUpdate: true,
|
||||
};
|
||||
case actions.SET_HAS_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
hasChanged: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -65,6 +65,43 @@ export default function (state = initialState, action) {
|
|||
},
|
||||
}),
|
||||
});
|
||||
case actions.ASSET_REMOVE:
|
||||
const claim = action.data;
|
||||
const newAssetList = state.assetList;
|
||||
delete newAssetList[`a#${claim.name}#${claim.claimId}`];
|
||||
|
||||
const channelId = `c#${claim.channelName}#${claim.certificateId}`;
|
||||
const channelClaims = state.channelList[channelId].claimsData.claims;
|
||||
const newClaimsData = channelClaims.filter(c => c.claimId !== claim.claimId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
assetList : newAssetList,
|
||||
channelList: {
|
||||
...state.channelList,
|
||||
[channelId]: {
|
||||
...state.channelList[channelId],
|
||||
claimsData: {
|
||||
...state.channelList[channelId].claimsData,
|
||||
claims: newClaimsData,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
case actions.ASSET_UPDATE_CLAIMDATA:
|
||||
return {
|
||||
...state,
|
||||
assetList: {
|
||||
...state.assetList,
|
||||
[action.data.id]: {
|
||||
...state.assetList[action.data.id],
|
||||
claimData: {
|
||||
...state.assetList[action.data.id].claimData,
|
||||
...action.data.claimData,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
// channel data
|
||||
case actions.CHANNEL_ADD:
|
||||
return Object.assign({}, state, {
|
||||
|
@ -77,7 +114,7 @@ export default function (state = initialState, action) {
|
|||
},
|
||||
}),
|
||||
});
|
||||
case actions.CHANNEL_CLAIMS_UPDATE_SUCCESS:
|
||||
case actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED:
|
||||
return Object.assign({}, state, {
|
||||
channelList: Object.assign({}, state.channelList, {
|
||||
[action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], {
|
||||
|
|
30
client/src/sagas/abandon.js
Normal file
30
client/src/sagas/abandon.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||
import * as actions from '../constants/publish_action_types';
|
||||
import * as publishStates from '../constants/publish_claim_states';
|
||||
import { updatePublishStatus, clearFile } from '../actions/publish';
|
||||
import { removeAsset } from '../actions/show';
|
||||
import { doAbandonClaim } from '../api/assetApi';
|
||||
|
||||
function * abandonClaim (action) {
|
||||
const { claimData, history } = action.data;
|
||||
const { claimId } = claimData;
|
||||
|
||||
const confirm = window.confirm('Are you sure you want to abandon this claim? This action cannot be undone.');
|
||||
if (!confirm) return;
|
||||
|
||||
yield put(updatePublishStatus(publishStates.ABANDONING, 'Your claim is being abandoned...'));
|
||||
|
||||
try {
|
||||
yield call(doAbandonClaim, claimId);
|
||||
} catch (error) {
|
||||
return console.log('abandon error:', error.message);
|
||||
}
|
||||
|
||||
yield put(clearFile());
|
||||
yield put(removeAsset(claimData));
|
||||
return history.push('/');
|
||||
}
|
||||
|
||||
export function * watchAbandonClaim () {
|
||||
yield takeLatest(actions.ABANDON_CLAIM, abandonClaim);
|
||||
};
|
|
@ -5,37 +5,57 @@ import { updateError, updatePublishStatus, clearFile } from '../actions/publish'
|
|||
import { selectPublishState } from '../selectors/publish';
|
||||
import { selectChannelState } from '../selectors/channel';
|
||||
import { selectSiteState } from '../selectors/site';
|
||||
import { selectShowState, selectAsset } from '../selectors/show';
|
||||
import { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';
|
||||
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';
|
||||
import { makePublishRequestChannel } from '../channels/publish';
|
||||
|
||||
function * publishFile (action) {
|
||||
const { history } = action.data;
|
||||
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, error: publishToolErrors } = yield select(selectPublishState);
|
||||
const publishState = yield select(selectPublishState);
|
||||
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, isUpdate, error: publishToolErrors } = publishState;
|
||||
const { loggedInChannel } = yield select(selectChannelState);
|
||||
const { host } = yield select(selectSiteState);
|
||||
|
||||
let show, asset;
|
||||
if (isUpdate) {
|
||||
show = yield select(selectShowState);
|
||||
asset = selectAsset(show);
|
||||
}
|
||||
// validate the channel selection
|
||||
try {
|
||||
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
|
||||
} catch (error) {
|
||||
return yield put(updateError('channel', error.message));
|
||||
};
|
||||
}
|
||||
// validate publish parameters
|
||||
try {
|
||||
validateNoPublishErrors(publishToolErrors);
|
||||
} catch (error) {
|
||||
return console.log('publish error:', error.message);
|
||||
}
|
||||
|
||||
let publishMetadata, publishFormData, publishChannel;
|
||||
// create metadata
|
||||
let publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
|
||||
publishMetadata = createPublishMetadata(
|
||||
isUpdate ? asset.name : claim,
|
||||
isUpdate ? {type: asset.claimData.contentType} : file,
|
||||
metadata,
|
||||
publishInChannel,
|
||||
selectedChannel
|
||||
);
|
||||
if (isUpdate) {
|
||||
publishMetadata['channelName'] = asset.claimData.channelName;
|
||||
}
|
||||
if (thumbnail) {
|
||||
// add thumbnail to publish metadata
|
||||
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
|
||||
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
|
||||
}
|
||||
// create form data for main publish
|
||||
const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
|
||||
publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
|
||||
// make the publish request
|
||||
const publishChannel = yield call(makePublishRequestChannel, publishFormData);
|
||||
publishChannel = yield call(makePublishRequestChannel, publishFormData, isUpdate);
|
||||
|
||||
while (true) {
|
||||
const {loadStart, progress, load, success, error: publishError} = yield take(publishChannel);
|
||||
if (publishError) {
|
||||
|
@ -43,7 +63,21 @@ function * publishFile (action) {
|
|||
}
|
||||
if (success) {
|
||||
yield put(clearFile());
|
||||
return history.push(`/${success.data.claimId}/${success.data.name}`);
|
||||
if (isUpdate) {
|
||||
yield put({
|
||||
type: 'ASSET_UPDATE_CLAIMDATA',
|
||||
data: {
|
||||
id : `a#${success.data.name}#${success.data.claimId}`,
|
||||
claimData: success.data.claimData,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (success.data.claimId) {
|
||||
return history.push(success.data.pushTo);
|
||||
} else {
|
||||
// this returns to the homepage, needs work
|
||||
return yield put(updatePublishStatus(publishStates.FAILED, 'ERROR'));
|
||||
}
|
||||
}
|
||||
if (loadStart) {
|
||||
yield put(updatePublishStatus(publishStates.LOAD_START, null));
|
||||
|
@ -55,7 +89,7 @@ function * publishFile (action) {
|
|||
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function * watchPublishStart () {
|
||||
yield takeLatest(actions.PUBLISH_START, publishFile);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { watchUpdateChannelAvailability } from './updateChannelAvailability';
|
|||
import { watchChannelCreate } from './createChannel';
|
||||
import { watchChannelLoginCheck } from './checkForLoggedInChannel';
|
||||
import { watchChannelLogout } from './logoutChannel';
|
||||
import { watchAbandonClaim } from './abandon';
|
||||
|
||||
export function * rootSaga () {
|
||||
yield all([
|
||||
|
@ -27,5 +28,6 @@ export function * rootSaga () {
|
|||
watchChannelLoginCheck(),
|
||||
watchChannelLogout(),
|
||||
watchUpdateAssetViews(),
|
||||
watchAbandonClaim(),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
export const selectAsset = (show) => {
|
||||
const request = show.requestList[show.request.id];
|
||||
const assetKey = request.key;
|
||||
return show.assetList[assetKey];
|
||||
export const selectAsset = show => {
|
||||
const requestId = show.request.id;
|
||||
let asset;
|
||||
const request = show.requestList[requestId] || null;
|
||||
const assetList = show.assetList;
|
||||
if (request && assetList) {
|
||||
const assetKey = request.key; // note: just store this in the request
|
||||
asset = assetList[assetKey] || null;
|
||||
}
|
||||
return asset;
|
||||
};
|
||||
|
||||
export const selectShowState = (state) => {
|
||||
|
|
10
client/src/utils/buildURI.js
Normal file
10
client/src/utils/buildURI.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const buildURI = asset => {
|
||||
let channelName, certificateId, name, claimId;
|
||||
if (asset.claimData) {
|
||||
({ channelName, certificateId, name, claimId } = asset.claimData);
|
||||
}
|
||||
if (channelName) {
|
||||
return `${channelName}:${certificateId}/${name}`;
|
||||
}
|
||||
return `${claimId}/${name}`;
|
||||
};
|
|
@ -16,7 +16,9 @@ export const createPublishMetadata = (claim, { type }, { title, description, lic
|
|||
export const createPublishFormData = (file, thumbnail, metadata) => {
|
||||
let fd = new FormData();
|
||||
// append file
|
||||
fd.append('file', file);
|
||||
if (file) {
|
||||
fd.append('file', file);
|
||||
}
|
||||
// append thumbnail
|
||||
if (thumbnail) {
|
||||
fd.append('thumbnail', thumbnail);
|
||||
|
@ -31,5 +33,5 @@ export const createPublishFormData = (file, thumbnail, metadata) => {
|
|||
};
|
||||
|
||||
export const createThumbnailUrl = (channel, channelId, claim, host) => {
|
||||
return `${host}/${channel}:${channelId}/${claim}-thumb.png`;
|
||||
return `${host}/${channel}:${channelId}/${claim}-thumb.jpg`;
|
||||
};
|
||||
|
|
|
@ -850,7 +850,7 @@ var claimQueries = (db, table, sequelize) => ({
|
|||
});
|
||||
},
|
||||
|
||||
getShortClaimIdFromLongClaimId: async (claimId, claimName) => {
|
||||
getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
|
||||
logger$1.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
|
||||
return await table.findAll({
|
||||
where: { name: claimName },
|
||||
|
@ -860,7 +860,12 @@ var claimQueries = (db, table, sequelize) => ({
|
|||
throw new Error('No claim(s) found with that claim name');
|
||||
}
|
||||
|
||||
return returnShortId(result, claimId);
|
||||
let list = result.map(claim => claim.dataValues);
|
||||
if (pendingClaim) {
|
||||
list = list.concat(pendingClaim);
|
||||
}
|
||||
|
||||
return returnShortId(list, claimId);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -981,6 +986,24 @@ var claimQueries = (db, table, sequelize) => ({
|
|||
});
|
||||
},
|
||||
|
||||
resolveClaimInChannel: async (claimName, channelId) => {
|
||||
logger$1.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
|
||||
return table.findAll({
|
||||
where: {
|
||||
name: claimName,
|
||||
publisher_id: channelId,
|
||||
},
|
||||
}).then(claimArray => {
|
||||
if (claimArray.length === 0) {
|
||||
return null;
|
||||
} else if (claimArray.length !== 1) {
|
||||
logger$1.warn(`more than one record matches ${claimName} in ${channelId}`);
|
||||
}
|
||||
|
||||
return claimArray[0];
|
||||
});
|
||||
},
|
||||
|
||||
getOutpoint: async (name, claimId) => {
|
||||
logger$1.debug(`finding outpoint for ${name}#${claimId}`);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export default (db, table, sequelize) => ({
|
|||
});
|
||||
},
|
||||
|
||||
getShortClaimIdFromLongClaimId: async (claimId, claimName) => {
|
||||
getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
|
||||
logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
|
||||
return await table.findAll({
|
||||
where: { name: claimName },
|
||||
|
@ -59,7 +59,12 @@ export default (db, table, sequelize) => ({
|
|||
throw new Error('No claim(s) found with that claim name');
|
||||
}
|
||||
|
||||
return returnShortId(result, claimId);
|
||||
let list = result.map(claim => claim.dataValues);
|
||||
if (pendingClaim) {
|
||||
list = list.concat(pendingClaim);
|
||||
}
|
||||
|
||||
return returnShortId(list, claimId);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -180,6 +185,24 @@ export default (db, table, sequelize) => ({
|
|||
});
|
||||
},
|
||||
|
||||
resolveClaimInChannel: async (claimName, channelId) => {
|
||||
logger.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
|
||||
return table.findAll({
|
||||
where: {
|
||||
name: claimName,
|
||||
publisher_id: channelId,
|
||||
},
|
||||
}).then(claimArray => {
|
||||
if (claimArray.length === 0) {
|
||||
return null;
|
||||
} else if (claimArray.length !== 1) {
|
||||
logger.warn(`more than one record matches ${claimName} in ${channelId}`);
|
||||
}
|
||||
|
||||
return claimArray[0];
|
||||
});
|
||||
},
|
||||
|
||||
getOutpoint: async (name, claimId) => {
|
||||
logger.debug(`finding outpoint for ${name}#${claimId}`);
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@ const { returnPaginatedChannelClaims } = require('./channelPagination.js');
|
|||
|
||||
const getChannelClaims = async (channelName, channelShortId, page) => {
|
||||
const channelId = await chainquery.claim.queries.getLongClaimId(channelName, channelShortId);
|
||||
const channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId);
|
||||
|
||||
let channelClaims;
|
||||
if (channelId) {
|
||||
channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId);
|
||||
}
|
||||
|
||||
const processingChannelClaims = channelClaims ? channelClaims.map((claim) => getClaimData(claim)) : [];
|
||||
const processedChannelClaims = await Promise.all(processingChannelClaims);
|
||||
|
|
44
server/controllers/api/claim/abandon/index.js
Normal file
44
server/controllers/api/claim/abandon/index.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const logger = require('winston');
|
||||
const db = require('server/models');
|
||||
const { abandonClaim } = require('server/lbrynet');
|
||||
const deleteFile = require('../publish/deleteFile.js');
|
||||
const authenticateUser = require('../publish/authentication.js');
|
||||
|
||||
/*
|
||||
route to abandon a claim through the daemon
|
||||
*/
|
||||
|
||||
const claimAbandon = async (req, res) => {
|
||||
const {claimId} = req.body;
|
||||
const {user} = req;
|
||||
try {
|
||||
const [channel, claim] = await Promise.all([
|
||||
authenticateUser(user.channelName, null, null, user),
|
||||
db.Claim.findOne({where: {claimId}}),
|
||||
]);
|
||||
|
||||
if (!claim) throw new Error('That channel does not exist');
|
||||
if (!channel.channelName) throw new Error('You don\'t own this channel');
|
||||
|
||||
await abandonClaim({claimId});
|
||||
const file = await db.File.findOne({where: {claimId}});
|
||||
await Promise.all([
|
||||
deleteFile(file.filePath),
|
||||
db.File.destroy({where: {claimId}}),
|
||||
db.Claim.destroy({where: {claimId}}),
|
||||
]);
|
||||
logger.debug(`Claim abandoned: ${claimId}`);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `Claim with id ${claimId} abandonded`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('abandon claim error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = claimAbandon;
|
|
@ -1,8 +1,8 @@
|
|||
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
|
||||
const getClaimData = require('server/utils/getClaimData');
|
||||
const fetchClaimData = require('server/utils/fetchClaimData');
|
||||
const chainquery = require('chainquery');
|
||||
const db = require('server/models');
|
||||
|
||||
/*
|
||||
|
||||
route to return data for a claim
|
||||
|
@ -10,16 +10,9 @@ const db = require('server/models');
|
|||
*/
|
||||
|
||||
const claimData = async ({ ip, originalUrl, body, params }, res) => {
|
||||
const claimName = params.claimName;
|
||||
let claimId = params.claimId;
|
||||
if (claimId === 'none') claimId = null;
|
||||
|
||||
try {
|
||||
let resolvedClaim = await chainquery.claim.queries.resolveClaim(claimName, claimId).catch(() => {});
|
||||
|
||||
if(!resolvedClaim) {
|
||||
resolvedClaim = await db.Claim.resolveClaim(claimName, claimId);
|
||||
}
|
||||
const resolvedClaim = await fetchClaimData(params);
|
||||
|
||||
if (!resolvedClaim) {
|
||||
return res.status(404).json({
|
||||
|
|
|
@ -11,7 +11,7 @@ const createPublishParams = (filePath, name, title, description, license, nsfw,
|
|||
}
|
||||
// provide default for license
|
||||
if (license === null || license.trim() === '') {
|
||||
license = ' '; // default to empty string
|
||||
license = ''; // default to empty string
|
||||
}
|
||||
// create the basic publish params
|
||||
const publishParams = {
|
||||
|
|
|
@ -17,6 +17,9 @@ const parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');
|
|||
const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
|
||||
const authenticateUser = require('./authentication.js');
|
||||
|
||||
const chainquery = require('chainquery');
|
||||
const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
|
||||
|
||||
const CLAIM_TAKEN = 'CLAIM_TAKEN';
|
||||
const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';
|
||||
|
||||
|
@ -42,7 +45,25 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
|
|||
});
|
||||
}
|
||||
// define variables
|
||||
let channelName, channelId, channelPassword, description, fileName, filePath, fileExtension, fileType, gaStartTime, license, name, nsfw, thumbnail, thumbnailFileName, thumbnailFilePath, thumbnailFileType, title;
|
||||
let channelName,
|
||||
channelId,
|
||||
channelPassword,
|
||||
description,
|
||||
fileName,
|
||||
filePath,
|
||||
fileExtension,
|
||||
fileType,
|
||||
gaStartTime,
|
||||
license,
|
||||
name,
|
||||
nsfw,
|
||||
thumbnail,
|
||||
thumbnailFileName,
|
||||
thumbnailFilePath,
|
||||
thumbnailFileType,
|
||||
title,
|
||||
claimData,
|
||||
claimId;
|
||||
// record the start time of the request
|
||||
gaStartTime = Date.now();
|
||||
// validate the body and files of the request
|
||||
|
@ -64,6 +85,7 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
|
|||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
checkClaimAvailability(name),
|
||||
createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName, channelClaimId),
|
||||
|
@ -83,19 +105,40 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
|
|||
publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
|
||||
}
|
||||
// publish the asset
|
||||
return publish(publishParams, fileName, fileType);
|
||||
return publish(publishParams, fileName, fileType, filePath);
|
||||
})
|
||||
.then(result => {
|
||||
.then(publishResults => {
|
||||
logger.info('Publish success >', publishResults);
|
||||
claimData = publishResults;
|
||||
({claimId} = claimData);
|
||||
|
||||
if (channelName) {
|
||||
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimData.certificateId, channelName);
|
||||
} else {
|
||||
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimId, name, claimData).catch(error => {
|
||||
return claimId.slice(0, 1);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(shortId => {
|
||||
let canonicalUrl;
|
||||
if (channelName) {
|
||||
canonicalUrl = createCanonicalLink({ asset: { ...claimData, channelShortId: shortId } });
|
||||
} else {
|
||||
canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId } })
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'publish completed successfully',
|
||||
data : {
|
||||
name,
|
||||
claimId : result.claim_id,
|
||||
url : `${host}/${result.claim_id}/${name}`, // for backwards compatability with app
|
||||
showUrl : `${host}/${result.claim_id}/${name}`,
|
||||
serveUrl: `${host}/${result.claim_id}/${name}${fileExtension}`,
|
||||
lbryTx : result,
|
||||
claimId,
|
||||
url : `${host}${canonicalUrl}`, // for backwards compatability with app
|
||||
showUrl : `${host}${canonicalUrl}`,
|
||||
serveUrl: `${host}${canonicalUrl}${fileExtension}`,
|
||||
pushTo : canonicalUrl,
|
||||
claimData,
|
||||
},
|
||||
});
|
||||
// record the publish end time and send to google analytics
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
const path = require('path');
|
||||
const validateFileTypeAndSize = require('./validateFileTypeAndSize.js');
|
||||
|
||||
const parsePublishApiRequestFiles = ({file, thumbnail}) => {
|
||||
const parsePublishApiRequestFiles = ({file, thumbnail}, isUpdate) => {
|
||||
// make sure a file was provided
|
||||
if (!file) {
|
||||
if (isUpdate) {
|
||||
if (thumbnail) {
|
||||
const obj = {};
|
||||
obj.thumbnailFileName = thumbnail.name;
|
||||
obj.thumbnailFilePath = thumbnail.path;
|
||||
obj.thumbnailFileType = thumbnail.type;
|
||||
return obj;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
throw new Error('no file with key of [file] found in request');
|
||||
}
|
||||
if (!file.path) {
|
||||
|
@ -28,18 +38,24 @@ const parsePublishApiRequestFiles = ({file, thumbnail}) => {
|
|||
if (/'/.test(file.name)) {
|
||||
throw new Error('apostrophes are not allowed in the file name');
|
||||
}
|
||||
|
||||
// validate the file
|
||||
validateFileTypeAndSize(file);
|
||||
if (file) validateFileTypeAndSize(file);
|
||||
// return results
|
||||
return {
|
||||
fileName : file.name,
|
||||
filePath : file.path,
|
||||
fileExtension : path.extname(file.path),
|
||||
fileType : file.type,
|
||||
thumbnailFileName: (thumbnail ? thumbnail.name : null),
|
||||
thumbnailFilePath: (thumbnail ? thumbnail.path : null),
|
||||
thumbnailFileType: (thumbnail ? thumbnail.type : null),
|
||||
const obj = {
|
||||
fileName : file.name,
|
||||
filePath : file.path,
|
||||
fileExtension: path.extname(file.path),
|
||||
fileType : file.type,
|
||||
};
|
||||
|
||||
if (thumbnail) {
|
||||
obj.thumbnailFileName = thumbnail.name;
|
||||
obj.thumbnailFilePath = thumbnail.path;
|
||||
obj.thumbnailFileType = thumbnail.type;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
module.exports = parsePublishApiRequestFiles;
|
||||
|
|
|
@ -1,81 +1,72 @@
|
|||
const logger = require('winston');
|
||||
const { publishClaim } = require('../../../../lbrynet');
|
||||
const db = require('../../../../models');
|
||||
const { publishClaim } = require('../../../../lbrynet');
|
||||
const { createFileRecordDataAfterPublish } = require('../../../../models/utils/createFileRecordData.js');
|
||||
const { createClaimRecordDataAfterPublish } = require('../../../../models/utils/createClaimRecordData.js');
|
||||
const deleteFile = require('./deleteFile.js');
|
||||
|
||||
const publish = (publishParams, fileName, fileType) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let publishResults, certificateId, channelName;
|
||||
// publish the file
|
||||
return publishClaim(publishParams)
|
||||
.then(result => {
|
||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, result);
|
||||
const publish = async (publishParams, fileName, fileType) => {
|
||||
let publishResults;
|
||||
let channel;
|
||||
let fileRecord;
|
||||
let newFile = Boolean(publishParams.file_path);
|
||||
|
||||
// Support new daemon, TODO: remove
|
||||
publishResults = result.output && result.output.claim_id ? result.output : result;
|
||||
|
||||
// get the channel information
|
||||
if (publishParams.channel_name) {
|
||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||
return db.Channel.findOne({
|
||||
where: {
|
||||
channelName: publishParams.channel_name,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logger.debug('this claim was not published in a channel');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.then(channel => {
|
||||
// set channel information
|
||||
certificateId = null;
|
||||
channelName = null;
|
||||
if (channel) {
|
||||
certificateId = channel.channelClaimId;
|
||||
channelName = channel.channelName;
|
||||
}
|
||||
logger.debug(`certificateId: ${certificateId}`);
|
||||
})
|
||||
.then(() => {
|
||||
return Promise.all([
|
||||
createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults),
|
||||
createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults),
|
||||
]);
|
||||
})
|
||||
.then(([fileRecord, claimRecord]) => {
|
||||
// upsert the records
|
||||
const {name} = publishParams;
|
||||
const {claim_id: claimId} = publishResults;
|
||||
const upsertCriteria = {
|
||||
name,
|
||||
claimId,
|
||||
};
|
||||
return Promise.all([
|
||||
db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
|
||||
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
|
||||
]);
|
||||
})
|
||||
.then(([file, claim]) => {
|
||||
logger.debug('File and Claim records successfully created');
|
||||
return Promise.all([
|
||||
file.setClaim(claim),
|
||||
claim.setFile(file),
|
||||
]);
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('File and Claim records successfully associated');
|
||||
// resolve the promise with the result from lbryApi publishClaim;
|
||||
resolve(publishResults);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('PUBLISH ERROR', error);
|
||||
deleteFile(publishParams.file_path); // delete the local file
|
||||
reject(error);
|
||||
try {
|
||||
publishResults = await publishClaim(publishParams);
|
||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, publishResults);
|
||||
const outpoint = `${publishResults.output.txid}:${publishResults.output.nout}`;
|
||||
// get the channel information
|
||||
if (publishParams.channel_name) {
|
||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||
channel = await db.Channel.findOne({
|
||||
where: {
|
||||
channelName: publishParams.channel_name,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
channel = null;
|
||||
}
|
||||
const certificateId = channel ? channel.channelClaimId : null;
|
||||
const channelName = channel ? channel.channelName : null;
|
||||
|
||||
const claimRecord = await createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults);
|
||||
const {claimId} = claimRecord;
|
||||
const upsertCriteria = {name: publishParams.name, claimId};
|
||||
if (newFile) {
|
||||
// this is the problem
|
||||
//
|
||||
fileRecord = await createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults);
|
||||
} else {
|
||||
fileRecord = await db.File.findOne({where: {claimId}}).then(result => result.dataValues);
|
||||
}
|
||||
|
||||
const [file, claim] = await Promise.all([
|
||||
db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
|
||||
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
|
||||
]);
|
||||
logger.info(`File and Claim records successfully created (${publishParams.name})`);
|
||||
|
||||
await Promise.all([
|
||||
file.setClaim(claim),
|
||||
claim.setFile(file),
|
||||
]);
|
||||
logger.info(`File and Claim records successfully associated (${publishParams.name})`);
|
||||
|
||||
return Object.assign({}, claimRecord, {outpoint});
|
||||
} catch (err) {
|
||||
// parse daemon response when err is a string
|
||||
// this needs work
|
||||
logger.info('publish/publish err:', err);
|
||||
const error = typeof err === 'string' ? JSON.parse(err) : err;
|
||||
if (publishParams.file_path) {
|
||||
await deleteFile(publishParams.file_path);
|
||||
}
|
||||
const message = error.error && error.error.message ? error.error.message : 'Unknown publish error';
|
||||
return {
|
||||
error: true,
|
||||
message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = publish;
|
||||
|
|
199
server/controllers/api/claim/update/index.js
Normal file
199
server/controllers/api/claim/update/index.js
Normal file
|
@ -0,0 +1,199 @@
|
|||
const logger = require('winston');
|
||||
const db = require('server/models');
|
||||
const { details, publishing: { disabled, disabledMessage, primaryClaimAddress } } = require('@config/siteConfig');
|
||||
const { resolveUri } = require('server/lbrynet');
|
||||
const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');
|
||||
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
|
||||
const publish = require('../publish/publish.js');
|
||||
const parsePublishApiRequestBody = require('../publish/parsePublishApiRequestBody');
|
||||
const parsePublishApiRequestFiles = require('../publish/parsePublishApiRequestFiles.js');
|
||||
const authenticateUser = require('../publish/authentication.js');
|
||||
const createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js');
|
||||
const chainquery = require('chainquery');
|
||||
const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
|
||||
|
||||
/*
|
||||
route to update a claim through the daemon
|
||||
*/
|
||||
|
||||
const updateMetadata = ({nsfw, license, title, description}) => {
|
||||
const update = {};
|
||||
if (nsfw) update['nsfw'] = nsfw;
|
||||
if (license) update['license'] = license;
|
||||
if (title) update['title'] = title;
|
||||
if (description) update['description'] = description;
|
||||
return update;
|
||||
};
|
||||
|
||||
const rando = () => {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 6; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
||||
return text;
|
||||
};
|
||||
|
||||
const claimUpdate = ({ body, files, headers, ip, originalUrl, user, tor }, res) => {
|
||||
// logging
|
||||
logger.info('Claim update request:', {
|
||||
ip,
|
||||
headers,
|
||||
body,
|
||||
files,
|
||||
user,
|
||||
});
|
||||
|
||||
// check for disabled publishing
|
||||
if (disabled) {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: disabledMessage,
|
||||
});
|
||||
}
|
||||
|
||||
// define variables
|
||||
let channelName,
|
||||
channelId,
|
||||
channelPassword,
|
||||
description,
|
||||
fileName,
|
||||
filePath,
|
||||
fileType,
|
||||
gaStartTime,
|
||||
thumbnail,
|
||||
fileExtension,
|
||||
license,
|
||||
name,
|
||||
nsfw,
|
||||
thumbnailFileName,
|
||||
thumbnailFilePath,
|
||||
thumbnailFileType,
|
||||
title,
|
||||
claimRecord,
|
||||
metadata,
|
||||
publishResult,
|
||||
thumbnailUpdate = false;
|
||||
// record the start time of the request
|
||||
gaStartTime = Date.now();
|
||||
|
||||
try {
|
||||
({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body));
|
||||
({fileName, filePath, fileExtension, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType} = parsePublishApiRequestFiles(files, true));
|
||||
({channelName, channelId, channelPassword} = body);
|
||||
} catch (error) {
|
||||
return res.status(400).json({success: false, message: error.message});
|
||||
}
|
||||
|
||||
// check channel authorization
|
||||
authenticateUser(channelName, channelId, channelPassword, user)
|
||||
.then(({ channelName, channelClaimId }) => {
|
||||
if (!channelId) {
|
||||
channelId = channelClaimId;
|
||||
}
|
||||
return chainquery.claim.queries.resolveClaimInChannel(name, channelClaimId).then(claim => claim.dataValues);
|
||||
})
|
||||
.then(claim => {
|
||||
claimRecord = claim;
|
||||
if (claimRecord.content_type === 'video/mp4' && files.file) {
|
||||
thumbnailUpdate = true;
|
||||
}
|
||||
|
||||
if (!files.file || thumbnailUpdate) {
|
||||
return Promise.all([
|
||||
db.File.findOne({ where: { name, claimId: claim.claim_id } }),
|
||||
resolveUri(`${claim.name}#${claim.claim_id}`),
|
||||
]);
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
})
|
||||
.then(([fileResult, resolution]) => {
|
||||
|
||||
metadata = Object.assign({}, {
|
||||
title : claimRecord.title,
|
||||
description: claimRecord.description,
|
||||
nsfw : claimRecord.nsfw,
|
||||
license : claimRecord.license,
|
||||
language : 'en',
|
||||
author : details.title,
|
||||
}, updateMetadata({title, description, nsfw, license}));
|
||||
const publishParams = {
|
||||
name,
|
||||
bid : '0.01',
|
||||
claim_address: primaryClaimAddress,
|
||||
channel_name : channelName,
|
||||
channel_id : channelId,
|
||||
metadata,
|
||||
};
|
||||
|
||||
if (files.file) {
|
||||
if (thumbnailUpdate) {
|
||||
// publish new thumbnail
|
||||
const newThumbnailName = `${name}-${rando()}`;
|
||||
const newThumbnailParams = createThumbnailPublishParams(filePath, newThumbnailName, license, nsfw);
|
||||
newThumbnailParams['file_path'] = filePath;
|
||||
publish(newThumbnailParams, fileName, fileType);
|
||||
|
||||
publishParams['sources'] = resolution.claim.value.stream.source;
|
||||
publishParams['thumbnail'] = `${details.host}/${newThumbnailParams.channel_name}:${newThumbnailParams.channel_id}/${newThumbnailName}-thumb.jpg`;
|
||||
} else {
|
||||
publishParams['file_path'] = filePath;
|
||||
}
|
||||
} else {
|
||||
fileName = fileResult.fileName;
|
||||
fileType = fileResult.fileType;
|
||||
publishParams['sources'] = resolution.claim.value.stream.source;
|
||||
publishParams['thumbnail'] = claimRecord.thumbnail_url;
|
||||
}
|
||||
|
||||
const fp = files && files.file && files.file.path ? files.file.path : undefined;
|
||||
return publish(publishParams, fileName, fileType, fp);
|
||||
})
|
||||
.then(result => {
|
||||
publishResult = result;
|
||||
|
||||
if (channelName) {
|
||||
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.certificateId, channelName);
|
||||
} else {
|
||||
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.claimId, name, result).catch(error => {
|
||||
return result.claimId.slice(0, 1);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(shortId => {
|
||||
let canonicalUrl;
|
||||
if (channelName) {
|
||||
canonicalUrl = createCanonicalLink({ asset: { ...publishResult, channelShortId: shortId } });
|
||||
} else {
|
||||
canonicalUrl = createCanonicalLink({ asset: { ...publishResult, shortId } })
|
||||
}
|
||||
|
||||
if (publishResult.error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: publishResult.message,
|
||||
});
|
||||
}
|
||||
|
||||
const {claimId} = publishResult;
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'update successful',
|
||||
data : {
|
||||
name,
|
||||
claimId,
|
||||
url : `${details.host}${canonicalUrl}`, // for backwards compatability with app
|
||||
showUrl : `${details.host}${canonicalUrl}`,
|
||||
serveUrl: `${details.host}${canonicalUrl}${fileExtension}`,
|
||||
pushTo : canonicalUrl,
|
||||
claimData: publishResult,
|
||||
},
|
||||
});
|
||||
// record the publish end time and send to google analytics
|
||||
sendGATimingEvent('end-to-end', 'update', fileType, gaStartTime, Date.now());
|
||||
})
|
||||
.catch(error => {
|
||||
handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = claimUpdate;
|
|
@ -2,6 +2,7 @@ const axios = require('axios');
|
|||
const logger = require('winston');
|
||||
const { apiHost, apiPort, getTimeout } = require('@config/lbryConfig');
|
||||
const lbrynetUri = 'http://' + apiHost + ':' + apiPort;
|
||||
const db = require('../models');
|
||||
const { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('../utils/googleAnalytics.js');
|
||||
const handleLbrynetResponse = require('./utils/handleLbrynetResponse.js');
|
||||
const { publishing } = require('@config/siteConfig');
|
||||
|
@ -46,6 +47,21 @@ module.exports = {
|
|||
});
|
||||
});
|
||||
},
|
||||
async abandonClaim ({claimId}) {
|
||||
logger.debug(`lbryApi >> Abandon claim "${claimId}"`);
|
||||
const gaStartTime = Date.now();
|
||||
try {
|
||||
const abandon = await axios.post(lbrynetUri, {
|
||||
method: 'claim_abandon',
|
||||
params: { claim_id: claimId },
|
||||
});
|
||||
sendGATimingEvent('lbrynet', 'abandonClaim', 'ABANDON_CLAIM', gaStartTime, Date.now());
|
||||
return abandon.data;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return error;
|
||||
}
|
||||
},
|
||||
getClaimList (claimName) {
|
||||
logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`);
|
||||
const gaStartTime = Date.now();
|
||||
|
@ -75,7 +91,13 @@ module.exports = {
|
|||
})
|
||||
.then(({ data }) => {
|
||||
sendGATimingEvent('lbrynet', 'resolveUri', 'RESOLVE', gaStartTime, Date.now());
|
||||
if (data.result[uri].error) { // check for errors
|
||||
if (Object.keys(data.result).length === 0 && data.result.constructor === Object) {
|
||||
// workaround for daemon returning empty result object
|
||||
// https://github.com/lbryio/lbry/issues/1485
|
||||
db.Claim.findOne({ where: { claimId: uri.split('#')[1] } })
|
||||
.then(() => reject('This claim has not yet been confirmed on the LBRY blockchain'))
|
||||
.catch(() => reject(`Claim ${uri} does not exist`));
|
||||
} else if (data.result[uri].error) { // check for errors
|
||||
reject(data.result[uri].error);
|
||||
} else { // if no errors, resolve
|
||||
resolve(data.result[uri]);
|
||||
|
|
|
@ -28,7 +28,7 @@ async function createFileRecordDataAfterGet (resolveResult, getResult) {
|
|||
filePath,
|
||||
fileType,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function createFileRecordDataAfterPublish (fileName, fileType, publishParams, publishResults) {
|
||||
const {
|
||||
|
|
|
@ -13,6 +13,8 @@ const claimGet = require('../../controllers/api/claim/get');
|
|||
const claimList = require('../../controllers/api/claim/list');
|
||||
const claimLongId = require('../../controllers/api/claim/longId');
|
||||
const claimPublish = require('../../controllers/api/claim/publish');
|
||||
const claimAbandon = require('../../controllers/api/claim/abandon');
|
||||
const claimUpdate = require('../../controllers/api/claim/update');
|
||||
const claimResolve = require('../../controllers/api/claim/resolve');
|
||||
const claimShortId = require('../../controllers/api/claim/shortId');
|
||||
const claimViews = require('../../controllers/api/claim/views');
|
||||
|
@ -29,12 +31,10 @@ const getOEmbedData = require('../../controllers/api/oEmbed');
|
|||
module.exports = {
|
||||
// homepage routes
|
||||
'/api/homepage/data/channels': { controller: [ torCheckMiddleware, channelData ] },
|
||||
|
||||
// channel routes
|
||||
'/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] },
|
||||
'/api/channel/short-id/:longId/:name': { controller: [ torCheckMiddleware, channelShortId ] },
|
||||
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
|
||||
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
|
||||
'/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },
|
||||
|
||||
// sepcial routes
|
||||
|
@ -47,6 +47,8 @@ module.exports = {
|
|||
'/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] },
|
||||
'/api/claim/long-id': { method: 'post', controller: [ torCheckMiddleware, claimLongId ] }, // note: should be a 'get'
|
||||
'/api/claim/publish': { method: 'post', controller: [ torCheckMiddleware, autoblockPublishMiddleware, multipartMiddleware, autoblockPublishBodyMiddleware, claimPublish ] },
|
||||
'/api/claim/update': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimUpdate ] },
|
||||
'/api/claim/abandon': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimAbandon ] },
|
||||
'/api/claim/resolve/:name/:claimId': { controller: [ torCheckMiddleware, claimResolve ] },
|
||||
'/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] },
|
||||
'/api/claim/views/:claimId': { controller: [ torCheckMiddleware, claimViews ] },
|
||||
|
|
|
@ -15,6 +15,7 @@ module.exports = {
|
|||
'/trending': { controller: redirect('/popular') },
|
||||
'/popular': { controller: handlePageRequest },
|
||||
'/new': { controller: handlePageRequest },
|
||||
'/edit/:claimId': { controller: handlePageRequest },
|
||||
'/multisite': { controller: handlePageRequest },
|
||||
'/video-embed/:name/:claimId/:config?': { controller: handleVideoEmbedRequest }, // for twitter
|
||||
};
|
||||
|
|
25
server/utils/fetchClaimData.js
Normal file
25
server/utils/fetchClaimData.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const chainquery = require('chainquery');
|
||||
const db = require('server/models');
|
||||
|
||||
const fetchClaimData = async (params) => {
|
||||
let { claimId, claimName: name } = params;
|
||||
if (claimId === 'none') claimId = null;
|
||||
|
||||
const [cq, local] = await Promise.all([
|
||||
chainquery.claim.queries.resolveClaim(name, claimId).then(res => res.dataValues).catch(() => {}),
|
||||
db.Claim.resolveClaim(name, claimId).catch(() => {}),
|
||||
]);
|
||||
|
||||
if (!cq && !local) {
|
||||
return null;
|
||||
}
|
||||
if (cq && cq.name === name && !local) {
|
||||
return cq;
|
||||
}
|
||||
if (local && local.name === name && !cq) {
|
||||
return local;
|
||||
}
|
||||
return local.updatedAt > cq.modified_at ? local : cq;
|
||||
};
|
||||
|
||||
module.exports = fetchClaimData;
|
|
@ -25,7 +25,7 @@ module.exports = async (data) => {
|
|||
claimId: data.claim_id || data.claimId,
|
||||
fileExt: data.generated_extension || data.fileExt,
|
||||
description: data.description,
|
||||
thumbnail: data.generated_thumbnail || data.thumbnail,
|
||||
thumbnail: data.generated_thumbnail || data.thumbnail_url || data.thumbnail,
|
||||
outpoint: data.transaction_hash_id || data.outpoint,
|
||||
host,
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue