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) {
|
export function updateMetadata (name, value) {
|
||||||
return {
|
return {
|
||||||
type: actions.METADATA_UPDATE,
|
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) {
|
export function setPublishInChannel (channel) {
|
||||||
return {
|
return {
|
||||||
type: actions.SET_PUBLISH_IN_CHANNEL,
|
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
|
// channel actions
|
||||||
|
|
||||||
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {
|
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {
|
||||||
|
@ -129,7 +136,7 @@ export function onUpdateChannelClaims (channelKey, name, longId, page) {
|
||||||
|
|
||||||
export function updateChannelClaims (channelListId, claimsData) {
|
export function updateChannelClaims (channelListId, claimsData) {
|
||||||
return {
|
return {
|
||||||
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCESS,
|
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED,
|
||||||
data: {channelListId, claimsData},
|
data: {channelListId, claimsData},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,3 +42,15 @@ export function getClaimViews (claimId) {
|
||||||
const url = `/api/claim/views/${claimId}`;
|
const url = `/api/claim/views/${claimId}`;
|
||||||
return Request(url);
|
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 FourOhFourPage from '@pages/FourOhFourPage';
|
||||||
import MultisitePage from '@pages/MultisitePage';
|
import MultisitePage from '@pages/MultisitePage';
|
||||||
import PopularPage from '@pages/PopularPage';
|
import PopularPage from '@pages/PopularPage';
|
||||||
|
import EditPage from '@pages/EditPage';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -21,6 +22,7 @@ const App = () => {
|
||||||
<Route exact path='/login' component={LoginPage} />
|
<Route exact path='/login' component={LoginPage} />
|
||||||
<Route exact path='/multisite' component={MultisitePage} />
|
<Route exact path='/multisite' component={MultisitePage} />
|
||||||
<Route exact path='/popular' component={PopularPage} />
|
<Route exact path='/popular' component={PopularPage} />
|
||||||
|
<Route exact path='/edit/:identifier/:claim' component={EditPage} />
|
||||||
<Route exact path='/:identifier/:claim' component={ContentPageWrapper} />
|
<Route exact path='/:identifier/:claim' component={ContentPageWrapper} />
|
||||||
<Route exact path='/:claim' component={ContentPageWrapper} />
|
<Route exact path='/:claim' component={ContentPageWrapper} />
|
||||||
<Route component={FourOhFourPage} />
|
<Route component={FourOhFourPage} />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {buffers, END, eventChannel} from 'redux-saga';
|
import {buffers, END, eventChannel} from 'redux-saga';
|
||||||
|
|
||||||
export const makePublishRequestChannel = (fd) => {
|
export const makePublishRequestChannel = (fd, isUpdate) => {
|
||||||
return eventChannel(emitter => {
|
return eventChannel(emitter => {
|
||||||
const uri = '/api/claim/publish';
|
const uri = `/api/claim/${isUpdate ? 'update' : 'publish'}`;
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
// add event listeners
|
// add event listeners
|
||||||
const onLoadStart = () => {
|
const onLoadStart = () => {
|
||||||
|
|
|
@ -2,11 +2,14 @@ import React from 'react';
|
||||||
import FormFeedbackDisplay from '@components/FormFeedbackDisplay';
|
import FormFeedbackDisplay from '@components/FormFeedbackDisplay';
|
||||||
import Row from '@components/Row';
|
import Row from '@components/Row';
|
||||||
|
|
||||||
const DropzoneInstructionsDisplay = ({fileError}) => {
|
const DropzoneInstructionsDisplay = ({fileError, message}) => {
|
||||||
|
if (!message) {
|
||||||
|
message = 'Drag & drop image or video here to publish';
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={'dropzone-instructions-display'}>
|
<div className={'dropzone-instructions-display'}>
|
||||||
<Row>
|
<Row>
|
||||||
<p className={'text--large'}>Drag & drop image or video here to publish</p>
|
<p className={'text--large'}>{message}</p>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<p className={'text--small'}>OR</p>
|
<p className={'text--small'}>OR</p>
|
||||||
|
|
|
@ -10,7 +10,12 @@ class PublishPreview extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
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) {
|
componentWillReceiveProps (newProps) {
|
||||||
if (newProps.file !== this.props.file) {
|
if (newProps.file !== this.props.file) {
|
||||||
|
@ -54,8 +59,10 @@ class PublishPreview extends React.Component {
|
||||||
|
|
||||||
PublishPreview.propTypes = {
|
PublishPreview.propTypes = {
|
||||||
dimPreview: PropTypes.bool.isRequired,
|
dimPreview: PropTypes.bool.isRequired,
|
||||||
file : PropTypes.object.isRequired,
|
file : PropTypes.object,
|
||||||
thumbnail : PropTypes.object,
|
thumbnail : PropTypes.object,
|
||||||
|
isUpdate : PropTypes.bool,
|
||||||
|
sourceUrl : PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PublishPreview;
|
export default PublishPreview;
|
||||||
|
|
|
@ -16,7 +16,7 @@ const PublishLicenseInput = ({ handleSelect }) => {
|
||||||
className='select select--primary'
|
className='select select--primary'
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
>
|
>
|
||||||
<option value=' '>Unspecified</option>
|
<option value=''>Unspecified</option>
|
||||||
<option value='Public Domain'>Public Domain</option>
|
<option value='Public Domain'>Public Domain</option>
|
||||||
<option value='Creative Commons'>Creative Commons</option>
|
<option value='Creative Commons'>Creative Commons</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -7,9 +7,11 @@ import Row from '@components/Row';
|
||||||
|
|
||||||
class PublishPreview extends React.Component {
|
class PublishPreview extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
|
const { isUpdate, uri } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
|
{isUpdate && uri && (<p className='text--extra-small'>{`Editing ${uri}`}</p>)}
|
||||||
<PublishTitleInput />
|
<PublishTitleInput />
|
||||||
</Row>
|
</Row>
|
||||||
<HorizontalSplit
|
<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 THUMBNAIL_NEW = 'THUMBNAIL_NEW';
|
||||||
export const PUBLISH_START = 'PUBLISH_START';
|
export const PUBLISH_START = 'PUBLISH_START';
|
||||||
export const CLAIM_AVAILABILITY = 'CLAIM_AVAILABILITY';
|
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 LOAD_START = 'LOAD_START';
|
||||||
export const LOADING = 'LOADING';
|
export const LOADING = 'LOADING';
|
||||||
export const PUBLISHING = 'PUBLISHING';
|
export const PUBLISHING = 'PUBLISHING';
|
||||||
export const SUCCESS = 'SUCCESS';
|
export const SUCCEEDED = 'SUCCEEDED';
|
||||||
export const FAILED = 'FAILED';
|
export const FAILED = 'FAILED';
|
||||||
|
export const ABANDONING = 'ABANDONING';
|
||||||
|
|
|
@ -11,12 +11,14 @@ export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
|
||||||
// asset actions
|
// asset actions
|
||||||
export const ASSET_ADD = 'ASSET_ADD';
|
export const ASSET_ADD = 'ASSET_ADD';
|
||||||
export const ASSET_VIEWS_UPDATE = 'ASSET_VIEWS_UPDATE';
|
export const ASSET_VIEWS_UPDATE = 'ASSET_VIEWS_UPDATE';
|
||||||
|
export const ASSET_UPDATE_CLAIMDATA = 'ASSET_UPDATE_CLAIMDATA';
|
||||||
|
export const ASSET_REMOVE = 'ASSET_REMOVE';
|
||||||
|
|
||||||
// channel actions
|
// channel actions
|
||||||
export const CHANNEL_ADD = 'CHANNEL_ADD';
|
export const CHANNEL_ADD = 'CHANNEL_ADD';
|
||||||
|
|
||||||
export const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC';
|
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
|
// asset/file display actions
|
||||||
export const FILE_REQUESTED = 'FILE_REQUESTED';
|
export const FILE_REQUESTED = 'FILE_REQUESTED';
|
||||||
|
|
|
@ -3,7 +3,8 @@ import View from './view';
|
||||||
import { fileRequested } from '../../actions/show';
|
import { fileRequested } from '../../actions/show';
|
||||||
import { selectAsset } from '../../selectors/show';
|
import { selectAsset } from '../../selectors/show';
|
||||||
|
|
||||||
const mapStateToProps = ({ show }) => {
|
const mapStateToProps = (props) => {
|
||||||
|
const {show} = props;
|
||||||
// select error and status
|
// select error and status
|
||||||
const error = show.displayAsset.error;
|
const error = show.displayAsset.error;
|
||||||
const status = show.displayAsset.status;
|
const status = show.displayAsset.status;
|
||||||
|
|
|
@ -2,41 +2,11 @@ import React from 'react';
|
||||||
import Row from '@components/Row';
|
import Row from '@components/Row';
|
||||||
import ProgressBar from '@components/ProgressBar';
|
import ProgressBar from '@components/ProgressBar';
|
||||||
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';
|
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';
|
||||||
|
import createCanonicalLink from '../../../../utils/createCanonicalLink';
|
||||||
|
|
||||||
class AssetDisplay extends React.Component {
|
class AvailableContent extends React.Component {
|
||||||
componentDidMount () {
|
|
||||||
const { asset: { claimData: { name, claimId } } } = this.props;
|
|
||||||
this.props.onFileRequest(name, claimId);
|
|
||||||
}
|
|
||||||
render () {
|
render () {
|
||||||
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
|
const {contentType, sourceUrl, name, thumbnail} = this.props;
|
||||||
const sourceUrl = `/${claimId}/${name}.${fileExt}`;
|
|
||||||
return (
|
|
||||||
<div className={'asset-display'}>
|
|
||||||
{(status === LOCAL_CHECK) &&
|
|
||||||
<div>
|
|
||||||
<p>Checking to see if Spee.ch has your asset locally...</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === UNAVAILABLE) &&
|
|
||||||
<div>
|
|
||||||
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
|
||||||
<ProgressBar size={12} />
|
|
||||||
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === ERROR) &&
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the following error message in the <a className='link--primary' href='https://chat.lbry.io' target='_blank'>LBRY discord</a>.</p>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<p id='error-message'><i>{error}</i></p>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === AVAILABLE) &&
|
|
||||||
(() => {
|
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case 'image/jpeg':
|
case 'image/jpeg':
|
||||||
case 'image/jpg':
|
case 'image/jpg':
|
||||||
|
@ -66,7 +36,55 @@ class AssetDisplay extends React.Component {
|
||||||
<p>Unsupported content type</p>
|
<p>Unsupported content type</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetDisplay extends React.Component {
|
||||||
|
componentDidMount () {
|
||||||
|
const { asset: { claimData: { name, claimId } } } = this.props;
|
||||||
|
this.props.onFileRequest(name, claimId);
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
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) &&
|
||||||
|
<div>
|
||||||
|
<p>Checking to see if Spee.ch has your asset locally...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{(status === UNAVAILABLE) &&
|
||||||
|
<div>
|
||||||
|
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
||||||
|
<ProgressBar size={12} />
|
||||||
|
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{(status === ERROR) &&
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the following error message in the <a className='link--primary' href='https://chat.lbry.io' target='_blank'>LBRY discord</a>.</p>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<p id='error-message'><i>{error}</i></p>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{(status === AVAILABLE) &&
|
||||||
|
<AvailableContent
|
||||||
|
contentType={contentType}
|
||||||
|
sourceUrl={sourceUrl}
|
||||||
|
name={name}
|
||||||
|
thumbnail={thumbnail}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,12 +2,20 @@ import { connect } from 'react-redux';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
import { selectAsset } from '../../selectors/show';
|
import { selectAsset } from '../../selectors/show';
|
||||||
|
|
||||||
const mapStateToProps = ({ show }) => {
|
const mapStateToProps = (props) => {
|
||||||
|
const {show} = props;
|
||||||
// select asset
|
// select asset
|
||||||
const asset = selectAsset(show);
|
const asset = selectAsset(show);
|
||||||
|
const editable = Boolean(
|
||||||
|
asset &&
|
||||||
|
asset.claimData &&
|
||||||
|
asset.claimData.channelName &&
|
||||||
|
props.channel.loggedInChannel.name === asset.claimData.channelName
|
||||||
|
);
|
||||||
// return props
|
// return props
|
||||||
return {
|
return {
|
||||||
asset,
|
asset,
|
||||||
|
editable,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,11 @@ import createCanonicalLink from '../../../../utils/createCanonicalLink';
|
||||||
|
|
||||||
class AssetInfo extends React.Component {
|
class AssetInfo extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const { asset } = this.props;
|
const { editable, asset } = this.props;
|
||||||
const { claimViews, claimData: { channelName, channelShortId, description, name, fileExt, contentType, thumbnail, host } } = asset;
|
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}`;
|
const assetCanonicalUrl = `${host}${canonicalUrl}`;
|
||||||
|
|
||||||
let channelCanonicalUrl;
|
let channelCanonicalUrl;
|
||||||
|
@ -29,6 +30,15 @@ class AssetInfo extends React.Component {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{editable && (
|
||||||
|
<Row>
|
||||||
|
<RowLabeled
|
||||||
|
label={<Label value={'Edit:'} />}
|
||||||
|
content={<Link to={`/edit${canonicalUrl}`}>{name}</Link>}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
{channelName && (
|
{channelName && (
|
||||||
<Row>
|
<Row>
|
||||||
<RowLabeled
|
<RowLabeled
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { connect } from 'react-redux';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
import { selectAsset } from '../../selectors/show';
|
import { selectAsset } from '../../selectors/show';
|
||||||
|
|
||||||
const mapStateToProps = ({ show }) => {
|
const mapStateToProps = (props) => {
|
||||||
const { claimData: { title } } = selectAsset(show);
|
const { claimData: { title } } = selectAsset(props.show);
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import Row from '@components/Row';
|
import Row from '@components/Row';
|
||||||
|
|
||||||
const AssetTitle = ({ title }) => {
|
const AssetTitle = ({ title }) => {
|
||||||
|
|
|
@ -36,7 +36,7 @@ class ChannelClaimsDisplay extends React.Component {
|
||||||
<AssetPreview
|
<AssetPreview
|
||||||
defaultThumbnail={defaultThumbnail}
|
defaultThumbnail={defaultThumbnail}
|
||||||
claimData={claim}
|
claimData={claim}
|
||||||
key={`${claim.name}-${claim.id}`}
|
key={claim.claimId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,29 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectFile, updateError, clearFile } from '../../actions/publish';
|
import { selectFile, updateError, clearFile } from '../../actions/publish';
|
||||||
|
import { selectAsset } from '../../selectors/show';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
|
import siteConfig from '@config/siteConfig.json';
|
||||||
|
import createCanonicalLink from '../../../../utils/createCanonicalLink';
|
||||||
|
|
||||||
const mapStateToProps = ({ publish }) => {
|
const { assetDefaults: { thumbnail: defaultThumbnail } } = siteConfig;
|
||||||
return {
|
|
||||||
file : publish.file,
|
const mapStateToProps = ({ show, publish: { file, thumbnail, fileError, isUpdate } }) => {
|
||||||
thumbnail: publish.thumbnail,
|
const obj = { file, thumbnail, fileError, isUpdate };
|
||||||
fileError: publish.error.file,
|
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 => {
|
const mapDispatchToProps = dispatch => {
|
||||||
|
|
|
@ -81,7 +81,13 @@ class Dropzone extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
const { dragOver, mouseOver, dimPreview } = this.state;
|
||||||
|
const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;
|
||||||
return (
|
return (
|
||||||
|
<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'>
|
<div className='dropzone-wrapper'>
|
||||||
<form>
|
<form>
|
||||||
<input
|
<input
|
||||||
|
@ -95,7 +101,7 @@ class Dropzone extends React.Component {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
className={'dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')}
|
className={'dropzone' + (dragOver ? ' dropzone--drag-over' : '')}
|
||||||
onDrop={this.handleDrop}
|
onDrop={this.handleDrop}
|
||||||
onDragOver={this.handleDragOver}
|
onDragOver={this.handleDragOver}
|
||||||
onDragEnd={this.handleDragEnd}
|
onDragEnd={this.handleDragEnd}
|
||||||
|
@ -104,31 +110,42 @@ class Dropzone extends React.Component {
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
onClick={this.handleClick}>
|
onClick={this.handleClick}>
|
||||||
{this.props.file ? (
|
{file || isUpdate ? (
|
||||||
<div className={'dropzone-preview-wrapper'}>
|
<div className={'dropzone-preview-wrapper'}>
|
||||||
|
{file ? (
|
||||||
<DropzonePreviewImage
|
<DropzonePreviewImage
|
||||||
dimPreview={this.state.dimPreview}
|
dimPreview={dimPreview}
|
||||||
file={this.props.file}
|
file={file}
|
||||||
thumbnail={this.props.thumbnail}
|
thumbnail={thumbnail}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<DropzonePreviewImage
|
||||||
|
dimPreview
|
||||||
|
isUpdate
|
||||||
|
sourceUrl={sourceUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={'dropzone-preview-overlay'}>
|
<div className={'dropzone-preview-overlay'}>
|
||||||
{ this.state.dragOver ? <DropzoneDropItDisplay /> : null }
|
{ dragOver ? <DropzoneDropItDisplay /> : null }
|
||||||
{ this.state.mouseOver ? (
|
{ mouseOver ? (
|
||||||
<DropzoneInstructionsDisplay
|
<DropzoneInstructionsDisplay
|
||||||
fileError={this.props.fileError}
|
fileError={fileError}
|
||||||
|
message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}
|
||||||
/>
|
/>
|
||||||
) : null }
|
) : null }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
this.state.dragOver ? <DropzoneDropItDisplay /> : (
|
dragOver ? <DropzoneDropItDisplay /> : (
|
||||||
<DropzoneInstructionsDisplay
|
<DropzoneInstructionsDisplay
|
||||||
fileError={this.props.fileError}
|
fileError={fileError}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { connect } from 'react-redux';
|
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';
|
import View from './view';
|
||||||
|
|
||||||
const mapStateToProps = ({ channel, publish }) => {
|
const mapStateToProps = ({ show, publish }) => {
|
||||||
return {
|
return {
|
||||||
file: publish.file,
|
file: publish.file,
|
||||||
|
isUpdate: publish.isUpdate,
|
||||||
|
hasChanged: publish.hasChanged,
|
||||||
|
asset: selectAsset(show),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
clearFile,
|
clearFile,
|
||||||
startPublish,
|
startPublish,
|
||||||
|
abandonClaim,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||||
|
|
|
@ -1,26 +1,65 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import {Link, withRouter} from 'react-router-dom';
|
||||||
import PublishUrlInput from '@containers/PublishUrlInput';
|
import PublishUrlInput from '@containers/PublishUrlInput';
|
||||||
import PublishThumbnailInput from '@containers/PublishThumbnailInput';
|
import PublishThumbnailInput from '@containers/PublishThumbnailInput';
|
||||||
import PublishMetadataInputs from '@containers/PublishMetadataInputs';
|
import PublishMetadataInputs from '@containers/PublishMetadataInputs';
|
||||||
import ChannelSelect from '@containers/ChannelSelect';
|
import ChannelSelect from '@containers/ChannelSelect';
|
||||||
import Row from '@components/Row';
|
import Row from '@components/Row';
|
||||||
|
import Label from '@components/Label';
|
||||||
|
import RowLabeled from '@components/RowLabeled';
|
||||||
import ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';
|
import ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';
|
||||||
import ButtonTertiary from '@components/ButtonTertiary';
|
import ButtonTertiary from '@components/ButtonTertiary';
|
||||||
|
import ButtonSecondary from '@components/ButtonSecondary';
|
||||||
import SpaceAround from '@components/SpaceAround';
|
import SpaceAround from '@components/SpaceAround';
|
||||||
import PublishFinePrint from '@components/PublishFinePrint';
|
import PublishFinePrint from '@components/PublishFinePrint';
|
||||||
|
import { SAVE } from '../../constants/confirmation_messages';
|
||||||
|
|
||||||
class PublishDetails extends React.Component {
|
class PublishDetails extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onPublishSubmit = this.onPublishSubmit.bind(this);
|
this.onPublishSubmit = this.onPublishSubmit.bind(this);
|
||||||
|
this.abandonClaim = this.abandonClaim.bind(this);
|
||||||
|
this.onCancel = this.onCancel.bind(this);
|
||||||
}
|
}
|
||||||
onPublishSubmit () {
|
onPublishSubmit () {
|
||||||
this.props.startPublish(this.props.history);
|
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 () {
|
render () {
|
||||||
|
const {file, isUpdate, asset} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{isUpdate ? (asset && (
|
||||||
|
<Row>
|
||||||
|
<RowLabeled
|
||||||
|
label={
|
||||||
|
<Label value={'Channel:'} />
|
||||||
|
}
|
||||||
|
content={
|
||||||
|
<span className='text'>
|
||||||
|
{asset.claimData.channelName}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)) : (
|
||||||
|
<React.Fragment>
|
||||||
<Row>
|
<Row>
|
||||||
<PublishUrlInput />
|
<PublishUrlInput />
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -28,8 +67,10 @@ class PublishDetails extends React.Component {
|
||||||
<Row>
|
<Row>
|
||||||
<ChannelSelect />
|
<ChannelSelect />
|
||||||
</Row>
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
{ this.props.file.type === 'video/mp4' && (
|
{ file && file.type === 'video/mp4' && (
|
||||||
<Row>
|
<Row>
|
||||||
<PublishThumbnailInput />
|
<PublishThumbnailInput />
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -41,16 +82,27 @@ class PublishDetails extends React.Component {
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<ButtonPrimaryJumbo
|
<ButtonPrimaryJumbo
|
||||||
value={'Publish'}
|
value={isUpdate ? 'Update' : 'Publish'}
|
||||||
onClickHandler={this.onPublishSubmit}
|
onClickHandler={this.onPublishSubmit}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{isUpdate && (
|
||||||
|
<Row>
|
||||||
|
<SpaceAround>
|
||||||
|
<ButtonSecondary
|
||||||
|
value={'Abandon Claim'}
|
||||||
|
onClickHandler={this.abandonClaim}
|
||||||
|
/>
|
||||||
|
</SpaceAround>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<SpaceAround>
|
<SpaceAround>
|
||||||
<ButtonTertiary
|
<ButtonTertiary
|
||||||
value={'Cancel'}
|
value={'Cancel'}
|
||||||
onClickHandler={this.props.clearFile}
|
onClickHandler={this.onCancel}
|
||||||
/>
|
/>
|
||||||
</SpaceAround>
|
</SpaceAround>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -8,6 +8,7 @@ const mapStateToProps = ({ publish }) => {
|
||||||
description : publish.metadata.description,
|
description : publish.metadata.description,
|
||||||
license : publish.metadata.license,
|
license : publish.metadata.license,
|
||||||
nsfw : publish.metadata.nsfw,
|
nsfw : publish.metadata.nsfw,
|
||||||
|
isUpdate : publish.isUpdate,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -26,27 +26,30 @@ class PublishMetadataInputs extends React.Component {
|
||||||
this.props.onMetadataChange(name, selectedOption);
|
this.props.onMetadataChange(name, selectedOption);
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
const { showMetadataInputs, description, isUpdate, nsfw } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.props.showMetadataInputs && (
|
{(showMetadataInputs || isUpdate) && (
|
||||||
<div>
|
<div>
|
||||||
<PublishDescriptionInput
|
<PublishDescriptionInput
|
||||||
description={this.props.description}
|
description={description}
|
||||||
handleInput={this.handleInput}
|
handleInput={this.handleInput}
|
||||||
/>
|
/>
|
||||||
<PublishLicenseInput
|
<PublishLicenseInput
|
||||||
handleSelect={this.handleSelect}
|
handleSelect={this.handleSelect}
|
||||||
/>
|
/>
|
||||||
<PublishNsfwInput
|
<PublishNsfwInput
|
||||||
nsfw={this.props.nsfw}
|
nsfw={nsfw}
|
||||||
handleInput={this.handleInput}
|
handleInput={this.handleInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isUpdate && (
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
value={this.props.showMetadataInputs ? 'less' : 'more'}
|
value={showMetadataInputs ? 'less' : 'more'}
|
||||||
onClickHandler={this.toggleShowInputs}
|
onClickHandler={this.toggleShowInputs}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ class PublishStatus extends React.Component {
|
||||||
{status === publishStates.LOAD_START &&
|
{status === publishStates.LOAD_START &&
|
||||||
<div className={'status'}>
|
<div className={'status'}>
|
||||||
<Row>
|
<Row>
|
||||||
<p>le is loading to server</p>
|
<p>File is loading to server</p>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<p className={'text--secondary'}>0%</p>
|
<p className={'text--secondary'}>0%</p>
|
||||||
|
@ -42,7 +42,7 @@ class PublishStatus extends React.Component {
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{status === publishStates.SUCCESS &&
|
{status === publishStates.SUCCEEDED &&
|
||||||
<div className={'status'}>
|
<div className={'status'}>
|
||||||
<Row>
|
<Row>
|
||||||
<p>Your publish is complete! You are being redirected to it now.</p>
|
<p>Your publish is complete! You are being redirected to it now.</p>
|
||||||
|
@ -71,6 +71,13 @@ class PublishStatus extends React.Component {
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{status === publishStates.ABANDONING &&
|
||||||
|
<div className={'status'}>
|
||||||
|
<Row>
|
||||||
|
<p>Your claim is being abandoned.</p>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import View from './view';
|
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 {
|
return {
|
||||||
disabled: publish.disabled,
|
disabled: publish.disabled,
|
||||||
file : publish.file,
|
file: publish.file,
|
||||||
status : publish.status.status,
|
status: publish.status.status,
|
||||||
|
isUpdate: publish.isUpdate,
|
||||||
|
hasChanged: publish.hasChanged,
|
||||||
|
uri,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withRouter, Prompt } from 'react-router';
|
||||||
import Dropzone from '@containers/Dropzone';
|
import Dropzone from '@containers/Dropzone';
|
||||||
import PublishPreview from '@components/PublishPreview';
|
import PublishPreview from '@components/PublishPreview';
|
||||||
import PublishStatus from '@containers/PublishStatus';
|
import PublishStatus from '@containers/PublishStatus';
|
||||||
import PublishDisabledMessage from '@containers/PublishDisabledMessage';
|
import PublishDisabledMessage from '@containers/PublishDisabledMessage';
|
||||||
|
import { SAVE } from '../../constants/confirmation_messages';
|
||||||
|
|
||||||
class PublishTool extends React.Component {
|
class PublishTool extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
if (this.props.disabled) {
|
const {disabled, file, isUpdate, hasChanged, uri, status, location: currentLocation} = this.props;
|
||||||
|
if (disabled) {
|
||||||
return (
|
return (
|
||||||
<PublishDisabledMessage />
|
<PublishDisabledMessage />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (this.props.file) {
|
if (file || isUpdate) {
|
||||||
if (this.props.status) {
|
if (status) {
|
||||||
return (
|
return (
|
||||||
<PublishStatus />
|
<PublishStatus />
|
||||||
);
|
);
|
||||||
} else {
|
} 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 />;
|
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 React from 'react';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
import PageLayout from '@components/PageLayout';
|
import PageLayout from '@components/PageLayout';
|
||||||
import HorizontalSplit from '@components/HorizontalSplit';
|
import HorizontalSplit from '@components/HorizontalSplit';
|
||||||
import AboutSpeechOverview from '@components/AboutSpeechOverview';
|
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 { connect } from 'react-redux';
|
||||||
import { onHandleShowHomepage } from '../../actions/show';
|
import { onHandleShowHomepage } from '../../actions/show';
|
||||||
|
import { clearFile } from '../../actions/publish';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
|
|
||||||
const mapStateToProps = ({ show, site, channel }) => {
|
const mapStateToProps = ({ show, site, channel, publish }) => {
|
||||||
return {
|
return {
|
||||||
error : show.request.error,
|
error : show.request.error,
|
||||||
requestType: show.request.type,
|
requestType: show.request.type,
|
||||||
homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,
|
homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,
|
||||||
|
isUpdate : publish.isUpdate,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
onHandleShowHomepage,
|
onHandleShowHomepage,
|
||||||
|
clearFile,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
|
@ -4,16 +4,9 @@ import PublishTool from '@containers/PublishTool';
|
||||||
import ContentPageWrapper from '@pages/ContentPageWrapper';
|
import ContentPageWrapper from '@pages/ContentPageWrapper';
|
||||||
|
|
||||||
class HomePage extends React.Component {
|
class HomePage extends React.Component {
|
||||||
componentDidMount () {
|
componentWillUnmount () {
|
||||||
this.props.onHandleShowHomepage(this.props.match.params);
|
this.props.clearFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.match.params !== this.props.match.params) {
|
|
||||||
this.props.onHandleShowHomepage(nextProps.match.params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { homeChannel } = this.props;
|
const { homeChannel } = this.props;
|
||||||
return homeChannel ? (
|
return homeChannel ? (
|
||||||
|
|
|
@ -1,20 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { selectAsset } from '../../selectors/show';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
|
|
||||||
const mapStateToProps = ({ show }) => {
|
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 {
|
return {
|
||||||
asset,
|
asset: selectAsset(show),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PageLayout from '@components/PageLayout';
|
import PageLayout from '@components/PageLayout';
|
||||||
|
|
||||||
import HorizontalSplit from '@components/HorizontalSplit';
|
import HorizontalSplit from '@components/HorizontalSplit';
|
||||||
import AssetTitle from '@containers/AssetTitle';
|
import AssetTitle from '@containers/AssetTitle';
|
||||||
import AssetDisplay from '@containers/AssetDisplay';
|
import AssetDisplay from '@containers/AssetDisplay';
|
||||||
|
|
|
@ -41,6 +41,8 @@ const initialState = {
|
||||||
license : '',
|
license : '',
|
||||||
nsfw : false,
|
nsfw : false,
|
||||||
},
|
},
|
||||||
|
isUpdate: false,
|
||||||
|
hasChanged: false,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
thumbnailChannel,
|
thumbnailChannel,
|
||||||
thumbnailChannelId,
|
thumbnailChannelId,
|
||||||
|
@ -49,8 +51,9 @@ const initialState = {
|
||||||
export default function (state = initialState, action) {
|
export default function (state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.FILE_SELECTED:
|
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,
|
file: action.data,
|
||||||
|
hasChanged: true,
|
||||||
});
|
});
|
||||||
case actions.FILE_CLEAR:
|
case actions.FILE_CLEAR:
|
||||||
return initialState;
|
return initialState;
|
||||||
|
@ -59,14 +62,17 @@ export default function (state = initialState, action) {
|
||||||
metadata: Object.assign({}, state.metadata, {
|
metadata: Object.assign({}, state.metadata, {
|
||||||
[action.data.name]: action.data.value,
|
[action.data.name]: action.data.value,
|
||||||
}),
|
}),
|
||||||
|
hasChanged: true,
|
||||||
});
|
});
|
||||||
case actions.CLAIM_UPDATE:
|
case actions.CLAIM_UPDATE:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
claim: action.data,
|
claim: action.data,
|
||||||
|
hasChanged: true,
|
||||||
});
|
});
|
||||||
case actions.SET_PUBLISH_IN_CHANNEL:
|
case actions.SET_PUBLISH_IN_CHANNEL:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
publishInChannel: action.channel,
|
publishInChannel: action.channel,
|
||||||
|
hasChanged: true,
|
||||||
});
|
});
|
||||||
case actions.PUBLISH_STATUS_UPDATE:
|
case actions.PUBLISH_STATUS_UPDATE:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
|
@ -83,13 +89,26 @@ export default function (state = initialState, action) {
|
||||||
selectedChannel: action.data,
|
selectedChannel: action.data,
|
||||||
});
|
});
|
||||||
case actions.TOGGLE_METADATA_INPUTS:
|
case actions.TOGGLE_METADATA_INPUTS:
|
||||||
return Object.assign({}, state, {
|
return {
|
||||||
|
...state,
|
||||||
showMetadataInputs: action.data,
|
showMetadataInputs: action.data,
|
||||||
});
|
};
|
||||||
case actions.THUMBNAIL_NEW:
|
case actions.THUMBNAIL_NEW:
|
||||||
return Object.assign({}, state, {
|
return {
|
||||||
|
...state,
|
||||||
thumbnail: action.data,
|
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:
|
default:
|
||||||
return state;
|
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
|
// channel data
|
||||||
case actions.CHANNEL_ADD:
|
case actions.CHANNEL_ADD:
|
||||||
return Object.assign({}, state, {
|
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, {
|
return Object.assign({}, state, {
|
||||||
channelList: Object.assign({}, state.channelList, {
|
channelList: Object.assign({}, state.channelList, {
|
||||||
[action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], {
|
[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 { selectPublishState } from '../selectors/publish';
|
||||||
import { selectChannelState } from '../selectors/channel';
|
import { selectChannelState } from '../selectors/channel';
|
||||||
import { selectSiteState } from '../selectors/site';
|
import { selectSiteState } from '../selectors/site';
|
||||||
|
import { selectShowState, selectAsset } from '../selectors/show';
|
||||||
import { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';
|
import { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';
|
||||||
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';
|
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';
|
||||||
import { makePublishRequestChannel } from '../channels/publish';
|
import { makePublishRequestChannel } from '../channels/publish';
|
||||||
|
|
||||||
function * publishFile (action) {
|
function * publishFile (action) {
|
||||||
const { history } = action.data;
|
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 { loggedInChannel } = yield select(selectChannelState);
|
||||||
const { host } = yield select(selectSiteState);
|
const { host } = yield select(selectSiteState);
|
||||||
|
|
||||||
|
let show, asset;
|
||||||
|
if (isUpdate) {
|
||||||
|
show = yield select(selectShowState);
|
||||||
|
asset = selectAsset(show);
|
||||||
|
}
|
||||||
// validate the channel selection
|
// validate the channel selection
|
||||||
try {
|
try {
|
||||||
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
|
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return yield put(updateError('channel', error.message));
|
return yield put(updateError('channel', error.message));
|
||||||
};
|
}
|
||||||
// validate publish parameters
|
// validate publish parameters
|
||||||
try {
|
try {
|
||||||
validateNoPublishErrors(publishToolErrors);
|
validateNoPublishErrors(publishToolErrors);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return console.log('publish error:', error.message);
|
return console.log('publish error:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let publishMetadata, publishFormData, publishChannel;
|
||||||
// create metadata
|
// 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) {
|
if (thumbnail) {
|
||||||
// add thumbnail to publish metadata
|
// 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
|
// create form data for main publish
|
||||||
const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
|
publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
|
||||||
// make the publish request
|
// make the publish request
|
||||||
const publishChannel = yield call(makePublishRequestChannel, publishFormData);
|
publishChannel = yield call(makePublishRequestChannel, publishFormData, isUpdate);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const {loadStart, progress, load, success, error: publishError} = yield take(publishChannel);
|
const {loadStart, progress, load, success, error: publishError} = yield take(publishChannel);
|
||||||
if (publishError) {
|
if (publishError) {
|
||||||
|
@ -43,7 +63,21 @@ function * publishFile (action) {
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
yield put(clearFile());
|
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) {
|
if (loadStart) {
|
||||||
yield put(updatePublishStatus(publishStates.LOAD_START, null));
|
yield put(updatePublishStatus(publishStates.LOAD_START, null));
|
||||||
|
@ -55,7 +89,7 @@ function * publishFile (action) {
|
||||||
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
|
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export function * watchPublishStart () {
|
export function * watchPublishStart () {
|
||||||
yield takeLatest(actions.PUBLISH_START, publishFile);
|
yield takeLatest(actions.PUBLISH_START, publishFile);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { watchUpdateChannelAvailability } from './updateChannelAvailability';
|
||||||
import { watchChannelCreate } from './createChannel';
|
import { watchChannelCreate } from './createChannel';
|
||||||
import { watchChannelLoginCheck } from './checkForLoggedInChannel';
|
import { watchChannelLoginCheck } from './checkForLoggedInChannel';
|
||||||
import { watchChannelLogout } from './logoutChannel';
|
import { watchChannelLogout } from './logoutChannel';
|
||||||
|
import { watchAbandonClaim } from './abandon';
|
||||||
|
|
||||||
export function * rootSaga () {
|
export function * rootSaga () {
|
||||||
yield all([
|
yield all([
|
||||||
|
@ -27,5 +28,6 @@ export function * rootSaga () {
|
||||||
watchChannelLoginCheck(),
|
watchChannelLoginCheck(),
|
||||||
watchChannelLogout(),
|
watchChannelLogout(),
|
||||||
watchUpdateAssetViews(),
|
watchUpdateAssetViews(),
|
||||||
|
watchAbandonClaim(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
export const selectAsset = (show) => {
|
export const selectAsset = show => {
|
||||||
const request = show.requestList[show.request.id];
|
const requestId = show.request.id;
|
||||||
const assetKey = request.key;
|
let asset;
|
||||||
return show.assetList[assetKey];
|
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) => {
|
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) => {
|
export const createPublishFormData = (file, thumbnail, metadata) => {
|
||||||
let fd = new FormData();
|
let fd = new FormData();
|
||||||
// append file
|
// append file
|
||||||
|
if (file) {
|
||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
|
}
|
||||||
// append thumbnail
|
// append thumbnail
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
fd.append('thumbnail', thumbnail);
|
fd.append('thumbnail', thumbnail);
|
||||||
|
@ -31,5 +33,5 @@ export const createPublishFormData = (file, thumbnail, metadata) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createThumbnailUrl = (channel, channelId, claim, host) => {
|
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}`);
|
logger$1.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
|
||||||
return await table.findAll({
|
return await table.findAll({
|
||||||
where: { name: claimName },
|
where: { name: claimName },
|
||||||
|
@ -860,7 +860,12 @@ var claimQueries = (db, table, sequelize) => ({
|
||||||
throw new Error('No claim(s) found with that claim name');
|
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) => {
|
getOutpoint: async (name, claimId) => {
|
||||||
logger$1.debug(`finding outpoint for ${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}`);
|
logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
|
||||||
return await table.findAll({
|
return await table.findAll({
|
||||||
where: { name: claimName },
|
where: { name: claimName },
|
||||||
|
@ -59,7 +59,12 @@ export default (db, table, sequelize) => ({
|
||||||
throw new Error('No claim(s) found with that claim name');
|
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) => {
|
getOutpoint: async (name, claimId) => {
|
||||||
logger.debug(`finding outpoint for ${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 getChannelClaims = async (channelName, channelShortId, page) => {
|
||||||
const channelId = await chainquery.claim.queries.getLongClaimId(channelName, channelShortId);
|
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 processingChannelClaims = channelClaims ? channelClaims.map((claim) => getClaimData(claim)) : [];
|
||||||
const processedChannelClaims = await Promise.all(processingChannelClaims);
|
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 { handleErrorResponse } = require('../../../utils/errorHandlers.js');
|
||||||
const getClaimData = require('server/utils/getClaimData');
|
const getClaimData = require('server/utils/getClaimData');
|
||||||
|
const fetchClaimData = require('server/utils/fetchClaimData');
|
||||||
const chainquery = require('chainquery');
|
const chainquery = require('chainquery');
|
||||||
const db = require('server/models');
|
const db = require('server/models');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
route to return data for a claim
|
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 claimData = async ({ ip, originalUrl, body, params }, res) => {
|
||||||
const claimName = params.claimName;
|
|
||||||
let claimId = params.claimId;
|
|
||||||
if (claimId === 'none') claimId = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let resolvedClaim = await chainquery.claim.queries.resolveClaim(claimName, claimId).catch(() => {});
|
const resolvedClaim = await fetchClaimData(params);
|
||||||
|
|
||||||
if(!resolvedClaim) {
|
|
||||||
resolvedClaim = await db.Claim.resolveClaim(claimName, claimId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resolvedClaim) {
|
if (!resolvedClaim) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
|
@ -11,7 +11,7 @@ const createPublishParams = (filePath, name, title, description, license, nsfw,
|
||||||
}
|
}
|
||||||
// provide default for license
|
// provide default for license
|
||||||
if (license === null || license.trim() === '') {
|
if (license === null || license.trim() === '') {
|
||||||
license = ' '; // default to empty string
|
license = ''; // default to empty string
|
||||||
}
|
}
|
||||||
// create the basic publish params
|
// create the basic publish params
|
||||||
const publishParams = {
|
const publishParams = {
|
||||||
|
|
|
@ -17,6 +17,9 @@ const parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');
|
||||||
const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
|
const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
|
||||||
const authenticateUser = require('./authentication.js');
|
const authenticateUser = require('./authentication.js');
|
||||||
|
|
||||||
|
const chainquery = require('chainquery');
|
||||||
|
const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
|
||||||
|
|
||||||
const CLAIM_TAKEN = 'CLAIM_TAKEN';
|
const CLAIM_TAKEN = 'CLAIM_TAKEN';
|
||||||
const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';
|
const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';
|
||||||
|
|
||||||
|
@ -42,7 +45,25 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// define variables
|
// 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
|
// record the start time of the request
|
||||||
gaStartTime = Date.now();
|
gaStartTime = Date.now();
|
||||||
// validate the body and files of the request
|
// validate the body and files of the request
|
||||||
|
@ -64,6 +85,7 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
|
||||||
};
|
};
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
checkClaimAvailability(name),
|
checkClaimAvailability(name),
|
||||||
createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName, channelClaimId),
|
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(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
|
||||||
}
|
}
|
||||||
// publish the asset
|
// 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({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'publish completed successfully',
|
message: 'publish completed successfully',
|
||||||
data : {
|
data : {
|
||||||
name,
|
name,
|
||||||
claimId : result.claim_id,
|
claimId,
|
||||||
url : `${host}/${result.claim_id}/${name}`, // for backwards compatability with app
|
url : `${host}${canonicalUrl}`, // for backwards compatability with app
|
||||||
showUrl : `${host}/${result.claim_id}/${name}`,
|
showUrl : `${host}${canonicalUrl}`,
|
||||||
serveUrl: `${host}/${result.claim_id}/${name}${fileExtension}`,
|
serveUrl: `${host}${canonicalUrl}${fileExtension}`,
|
||||||
lbryTx : result,
|
pushTo : canonicalUrl,
|
||||||
|
claimData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// record the publish end time and send to google analytics
|
// record the publish end time and send to google analytics
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const validateFileTypeAndSize = require('./validateFileTypeAndSize.js');
|
const validateFileTypeAndSize = require('./validateFileTypeAndSize.js');
|
||||||
|
|
||||||
const parsePublishApiRequestFiles = ({file, thumbnail}) => {
|
const parsePublishApiRequestFiles = ({file, thumbnail}, isUpdate) => {
|
||||||
// make sure a file was provided
|
// make sure a file was provided
|
||||||
if (!file) {
|
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');
|
throw new Error('no file with key of [file] found in request');
|
||||||
}
|
}
|
||||||
if (!file.path) {
|
if (!file.path) {
|
||||||
|
@ -28,18 +38,24 @@ const parsePublishApiRequestFiles = ({file, thumbnail}) => {
|
||||||
if (/'/.test(file.name)) {
|
if (/'/.test(file.name)) {
|
||||||
throw new Error('apostrophes are not allowed in the file name');
|
throw new Error('apostrophes are not allowed in the file name');
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the file
|
// validate the file
|
||||||
validateFileTypeAndSize(file);
|
if (file) validateFileTypeAndSize(file);
|
||||||
// return results
|
// return results
|
||||||
return {
|
const obj = {
|
||||||
fileName : file.name,
|
fileName : file.name,
|
||||||
filePath : file.path,
|
filePath : file.path,
|
||||||
fileExtension : path.extname(file.path),
|
fileExtension: path.extname(file.path),
|
||||||
fileType : file.type,
|
fileType : file.type,
|
||||||
thumbnailFileName: (thumbnail ? thumbnail.name : null),
|
|
||||||
thumbnailFilePath: (thumbnail ? thumbnail.path : null),
|
|
||||||
thumbnailFileType: (thumbnail ? thumbnail.type : null),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
obj.thumbnailFileName = thumbnail.name;
|
||||||
|
obj.thumbnailFilePath = thumbnail.path;
|
||||||
|
obj.thumbnailFileType = thumbnail.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = parsePublishApiRequestFiles;
|
module.exports = parsePublishApiRequestFiles;
|
||||||
|
|
|
@ -1,81 +1,72 @@
|
||||||
const logger = require('winston');
|
const logger = require('winston');
|
||||||
const { publishClaim } = require('../../../../lbrynet');
|
|
||||||
const db = require('../../../../models');
|
const db = require('../../../../models');
|
||||||
|
const { publishClaim } = require('../../../../lbrynet');
|
||||||
const { createFileRecordDataAfterPublish } = require('../../../../models/utils/createFileRecordData.js');
|
const { createFileRecordDataAfterPublish } = require('../../../../models/utils/createFileRecordData.js');
|
||||||
const { createClaimRecordDataAfterPublish } = require('../../../../models/utils/createClaimRecordData.js');
|
const { createClaimRecordDataAfterPublish } = require('../../../../models/utils/createClaimRecordData.js');
|
||||||
const deleteFile = require('./deleteFile.js');
|
const deleteFile = require('./deleteFile.js');
|
||||||
|
|
||||||
const publish = (publishParams, fileName, fileType) => {
|
const publish = async (publishParams, fileName, fileType) => {
|
||||||
return new Promise((resolve, reject) => {
|
let publishResults;
|
||||||
let publishResults, certificateId, channelName;
|
let channel;
|
||||||
// publish the file
|
let fileRecord;
|
||||||
return publishClaim(publishParams)
|
let newFile = Boolean(publishParams.file_path);
|
||||||
.then(result => {
|
|
||||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, result);
|
|
||||||
|
|
||||||
// Support new daemon, TODO: remove
|
|
||||||
publishResults = result.output && result.output.claim_id ? result.output : result;
|
|
||||||
|
|
||||||
|
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
|
// get the channel information
|
||||||
if (publishParams.channel_name) {
|
if (publishParams.channel_name) {
|
||||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||||
return db.Channel.findOne({
|
channel = await db.Channel.findOne({
|
||||||
where: {
|
where: {
|
||||||
channelName: publishParams.channel_name,
|
channelName: publishParams.channel_name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.debug('this claim was not published in a channel');
|
channel = null;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
})
|
const certificateId = channel ? channel.channelClaimId : null;
|
||||||
.then(channel => {
|
const channelName = channel ? channel.channelName : null;
|
||||||
// set channel information
|
|
||||||
certificateId = null;
|
const claimRecord = await createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults);
|
||||||
channelName = null;
|
const {claimId} = claimRecord;
|
||||||
if (channel) {
|
const upsertCriteria = {name: publishParams.name, claimId};
|
||||||
certificateId = channel.channelClaimId;
|
if (newFile) {
|
||||||
channelName = channel.channelName;
|
// this is the problem
|
||||||
|
//
|
||||||
|
fileRecord = await createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults);
|
||||||
|
} else {
|
||||||
|
fileRecord = await db.File.findOne({where: {claimId}}).then(result => result.dataValues);
|
||||||
}
|
}
|
||||||
logger.debug(`certificateId: ${certificateId}`);
|
|
||||||
})
|
const [file, claim] = await Promise.all([
|
||||||
.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.File, fileRecord, upsertCriteria, 'File'),
|
||||||
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
|
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
|
||||||
]);
|
]);
|
||||||
})
|
logger.info(`File and Claim records successfully created (${publishParams.name})`);
|
||||||
.then(([file, claim]) => {
|
|
||||||
logger.debug('File and Claim records successfully created');
|
await Promise.all([
|
||||||
return Promise.all([
|
|
||||||
file.setClaim(claim),
|
file.setClaim(claim),
|
||||||
claim.setFile(file),
|
claim.setFile(file),
|
||||||
]);
|
]);
|
||||||
})
|
logger.info(`File and Claim records successfully associated (${publishParams.name})`);
|
||||||
.then(() => {
|
|
||||||
logger.debug('File and Claim records successfully associated');
|
return Object.assign({}, claimRecord, {outpoint});
|
||||||
// resolve the promise with the result from lbryApi publishClaim;
|
} catch (err) {
|
||||||
resolve(publishResults);
|
// parse daemon response when err is a string
|
||||||
})
|
// this needs work
|
||||||
.catch(error => {
|
logger.info('publish/publish err:', err);
|
||||||
logger.error('PUBLISH ERROR', error);
|
const error = typeof err === 'string' ? JSON.parse(err) : err;
|
||||||
deleteFile(publishParams.file_path); // delete the local file
|
if (publishParams.file_path) {
|
||||||
reject(error);
|
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;
|
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 logger = require('winston');
|
||||||
const { apiHost, apiPort, getTimeout } = require('@config/lbryConfig');
|
const { apiHost, apiPort, getTimeout } = require('@config/lbryConfig');
|
||||||
const lbrynetUri = 'http://' + apiHost + ':' + apiPort;
|
const lbrynetUri = 'http://' + apiHost + ':' + apiPort;
|
||||||
|
const db = require('../models');
|
||||||
const { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('../utils/googleAnalytics.js');
|
const { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('../utils/googleAnalytics.js');
|
||||||
const handleLbrynetResponse = require('./utils/handleLbrynetResponse.js');
|
const handleLbrynetResponse = require('./utils/handleLbrynetResponse.js');
|
||||||
const { publishing } = require('@config/siteConfig');
|
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) {
|
getClaimList (claimName) {
|
||||||
logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`);
|
logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`);
|
||||||
const gaStartTime = Date.now();
|
const gaStartTime = Date.now();
|
||||||
|
@ -75,7 +91,13 @@ module.exports = {
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
sendGATimingEvent('lbrynet', 'resolveUri', 'RESOLVE', gaStartTime, Date.now());
|
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);
|
reject(data.result[uri].error);
|
||||||
} else { // if no errors, resolve
|
} else { // if no errors, resolve
|
||||||
resolve(data.result[uri]);
|
resolve(data.result[uri]);
|
||||||
|
|
|
@ -28,7 +28,7 @@ async function createFileRecordDataAfterGet (resolveResult, getResult) {
|
||||||
filePath,
|
filePath,
|
||||||
fileType,
|
fileType,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
async function createFileRecordDataAfterPublish (fileName, fileType, publishParams, publishResults) {
|
async function createFileRecordDataAfterPublish (fileName, fileType, publishParams, publishResults) {
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -13,6 +13,8 @@ const claimGet = require('../../controllers/api/claim/get');
|
||||||
const claimList = require('../../controllers/api/claim/list');
|
const claimList = require('../../controllers/api/claim/list');
|
||||||
const claimLongId = require('../../controllers/api/claim/longId');
|
const claimLongId = require('../../controllers/api/claim/longId');
|
||||||
const claimPublish = require('../../controllers/api/claim/publish');
|
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 claimResolve = require('../../controllers/api/claim/resolve');
|
||||||
const claimShortId = require('../../controllers/api/claim/shortId');
|
const claimShortId = require('../../controllers/api/claim/shortId');
|
||||||
const claimViews = require('../../controllers/api/claim/views');
|
const claimViews = require('../../controllers/api/claim/views');
|
||||||
|
@ -29,12 +31,10 @@ const getOEmbedData = require('../../controllers/api/oEmbed');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// homepage routes
|
// homepage routes
|
||||||
'/api/homepage/data/channels': { controller: [ torCheckMiddleware, channelData ] },
|
'/api/homepage/data/channels': { controller: [ torCheckMiddleware, channelData ] },
|
||||||
|
|
||||||
// channel routes
|
// channel routes
|
||||||
'/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] },
|
'/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] },
|
||||||
'/api/channel/short-id/:longId/:name': { controller: [ torCheckMiddleware, channelShortId ] },
|
'/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/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
|
|
||||||
'/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },
|
'/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },
|
||||||
|
|
||||||
// sepcial routes
|
// sepcial routes
|
||||||
|
@ -47,6 +47,8 @@ module.exports = {
|
||||||
'/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] },
|
'/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] },
|
||||||
'/api/claim/long-id': { method: 'post', controller: [ torCheckMiddleware, claimLongId ] }, // note: should be a 'get'
|
'/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/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/resolve/:name/:claimId': { controller: [ torCheckMiddleware, claimResolve ] },
|
||||||
'/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] },
|
'/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] },
|
||||||
'/api/claim/views/:claimId': { controller: [ torCheckMiddleware, claimViews ] },
|
'/api/claim/views/:claimId': { controller: [ torCheckMiddleware, claimViews ] },
|
||||||
|
|
|
@ -15,6 +15,7 @@ module.exports = {
|
||||||
'/trending': { controller: redirect('/popular') },
|
'/trending': { controller: redirect('/popular') },
|
||||||
'/popular': { controller: handlePageRequest },
|
'/popular': { controller: handlePageRequest },
|
||||||
'/new': { controller: handlePageRequest },
|
'/new': { controller: handlePageRequest },
|
||||||
|
'/edit/:claimId': { controller: handlePageRequest },
|
||||||
'/multisite': { controller: handlePageRequest },
|
'/multisite': { controller: handlePageRequest },
|
||||||
'/video-embed/:name/:claimId/:config?': { controller: handleVideoEmbedRequest }, // for twitter
|
'/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,
|
claimId: data.claim_id || data.claimId,
|
||||||
fileExt: data.generated_extension || data.fileExt,
|
fileExt: data.generated_extension || data.fileExt,
|
||||||
description: data.description,
|
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,
|
outpoint: data.transaction_hash_id || data.outpoint,
|
||||||
host,
|
host,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue