Publishing #158
10 changed files with 1949 additions and 592 deletions
1295
dist/bundle.es.js
vendored
1295
dist/bundle.es.js
vendored
File diff suppressed because it is too large
Load diff
51
dist/flow-typed/Publish.js
vendored
Normal file
51
dist/flow-typed/Publish.js
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type UpdatePublishFormData = {
|
||||||
|
filePath?: string,
|
||||||
|
contentIsFree?: boolean,
|
||||||
|
fee?: {
|
||||||
|
amount: string,
|
||||||
|
currency: string,
|
||||||
|
},
|
||||||
|
title?: string,
|
||||||
|
thumbnail_url?: string,
|
||||||
|
uploadThumbnailStatus?: string,
|
||||||
|
thumbnailPath?: string,
|
||||||
|
description?: string,
|
||||||
|
language?: string,
|
||||||
|
channel?: string,
|
||||||
|
channelId?: string,
|
||||||
|
name?: string,
|
||||||
|
nameError?: string,
|
||||||
|
bid?: number,
|
||||||
|
bidError?: string,
|
||||||
|
otherLicenseDescription?: string,
|
||||||
|
licenseUrl?: string,
|
||||||
|
licenseType?: string,
|
||||||
|
uri?: string,
|
||||||
|
nsfw: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type PublishParams = {
|
||||||
|
name: ?string,
|
||||||
|
bid: ?number,
|
||||||
|
filePath?: string,
|
||||||
|
description: ?string,
|
||||||
|
language: string,
|
||||||
|
publishingLicense?: string,
|
||||||
|
publishingLicenseUrl?: string,
|
||||||
|
thumbnail: ?string,
|
||||||
|
channel: string,
|
||||||
|
channelId?: string,
|
||||||
|
title: string,
|
||||||
|
contentIsFree: boolean,
|
||||||
|
uri?: string,
|
||||||
|
license: ?string,
|
||||||
|
licenseUrl: ?string,
|
||||||
|
fee?: {
|
||||||
|
amount: string,
|
||||||
|
currency: string,
|
||||||
|
},
|
||||||
|
claim: StreamClaim,
|
||||||
|
nsfw: boolean,
|
||||||
|
};
|
51
flow-typed/Publish.js
vendored
Normal file
51
flow-typed/Publish.js
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type UpdatePublishFormData = {
|
||||||
|
filePath?: string,
|
||||||
|
contentIsFree?: boolean,
|
||||||
|
fee?: {
|
||||||
|
amount: string,
|
||||||
|
currency: string,
|
||||||
|
},
|
||||||
|
title?: string,
|
||||||
|
thumbnail_url?: string,
|
||||||
|
uploadThumbnailStatus?: string,
|
||||||
|
thumbnailPath?: string,
|
||||||
|
description?: string,
|
||||||
|
language?: string,
|
||||||
|
channel?: string,
|
||||||
|
channelId?: string,
|
||||||
|
name?: string,
|
||||||
|
nameError?: string,
|
||||||
|
bid?: number,
|
||||||
|
bidError?: string,
|
||||||
|
otherLicenseDescription?: string,
|
||||||
|
licenseUrl?: string,
|
||||||
|
licenseType?: string,
|
||||||
|
uri?: string,
|
||||||
|
nsfw: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type PublishParams = {
|
||||||
|
name: ?string,
|
||||||
|
bid: ?number,
|
||||||
|
filePath?: string,
|
||||||
|
description: ?string,
|
||||||
|
language: string,
|
||||||
|
publishingLicense?: string,
|
||||||
|
publishingLicenseUrl?: string,
|
||||||
|
thumbnail: ?string,
|
||||||
|
channel: string,
|
||||||
|
channelId?: string,
|
||||||
|
title: string,
|
||||||
|
contentIsFree: boolean,
|
||||||
|
uri?: string,
|
||||||
|
license: ?string,
|
||||||
|
licenseUrl: ?string,
|
||||||
|
fee?: {
|
||||||
|
amount: string,
|
||||||
|
currency: string,
|
||||||
|
},
|
||||||
|
claim: StreamClaim,
|
||||||
|
nsfw: boolean,
|
||||||
|
};
|
5
src/constants/claim.js
Normal file
5
src/constants/claim.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const MINIMUM_PUBLISH_BID = 0.00000001;
|
||||||
|
|
||||||
|
export const CHANNEL_ANONYMOUS = 'anonymous';
|
||||||
|
export const CHANNEL_NEW = 'new';
|
||||||
|
export const PAGE_SIZE = 20;
|
31
src/constants/licenses.js
Normal file
31
src/constants/licenses.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export const CC_LICENSES = [
|
||||||
|
{
|
||||||
|
value: 'Creative Commons Attribution 4.0 International',
|
||||||
|
url: 'https://creativecommons.org/licenses/by/4.0/legalcode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Creative Commons Attribution-ShareAlike 4.0 International',
|
||||||
|
url: 'https://creativecommons.org/licenses/by-sa/4.0/legalcode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Creative Commons Attribution-NoDerivatives 4.0 International',
|
||||||
|
url: 'https://creativecommons.org/licenses/by-nd/4.0/legalcode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Creative Commons Attribution-NonCommercial 4.0 International',
|
||||||
|
url: 'https://creativecommons.org/licenses/by-nc/4.0/legalcode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International',
|
||||||
|
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International',
|
||||||
|
url: 'https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NONE = 'None';
|
||||||
|
export const PUBLIC_DOMAIN = 'Public Domain';
|
||||||
|
export const OTHER = 'other';
|
||||||
|
export const COPYRIGHT = 'copyright';
|
43
src/index.js
43
src/index.js
|
@ -1,9 +1,11 @@
|
||||||
|
import * as CLAIM_VALUES from 'constants/claim';
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
import * as LICENSES from 'constants/licenses';
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import * as TRANSACTIONS from 'constants/transaction_types';
|
|
||||||
import * as SORT_OPTIONS from 'constants/sort_options';
|
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import * as SORT_OPTIONS from 'constants/sort_options';
|
||||||
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
|
import * as TRANSACTIONS from 'constants/transaction_types';
|
||||||
import { SEARCH_TYPES, SEARCH_OPTIONS } from 'constants/search';
|
import { SEARCH_TYPES, SEARCH_OPTIONS } from 'constants/search';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
import { selectState as selectSearchState } from 'redux/selectors/search';
|
import { selectState as selectSearchState } from 'redux/selectors/search';
|
||||||
|
@ -11,6 +13,8 @@ import { selectState as selectSearchState } from 'redux/selectors/search';
|
||||||
// constants
|
// constants
|
||||||
export {
|
export {
|
||||||
ACTIONS,
|
ACTIONS,
|
||||||
|
CLAIM_VALUES,
|
||||||
|
LICENSES,
|
||||||
THUMBNAIL_STATUSES,
|
THUMBNAIL_STATUSES,
|
||||||
SEARCH_TYPES,
|
SEARCH_TYPES,
|
||||||
SEARCH_OPTIONS,
|
SEARCH_OPTIONS,
|
||||||
|
@ -57,6 +61,16 @@ export {
|
||||||
doSetFileListSort,
|
doSetFileListSort,
|
||||||
} from 'redux/actions/file_info';
|
} from 'redux/actions/file_info';
|
||||||
|
|
||||||
|
export {
|
||||||
|
doResetThumbnailStatus,
|
||||||
|
doClearPublish,
|
||||||
|
doUpdatePublishForm,
|
||||||
|
doUploadThumbnail,
|
||||||
|
doPrepareEdit,
|
||||||
|
doPublish,
|
||||||
|
doCheckPendingPublishes
|
||||||
|
} from 'redux/actions/publish';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
doSearch,
|
doSearch,
|
||||||
doUpdateSearchQuery,
|
doUpdateSearchQuery,
|
||||||
|
@ -100,14 +114,15 @@ export { isClaimNsfw } from 'util/claim';
|
||||||
|
|
||||||
// reducers
|
// reducers
|
||||||
export { claimsReducer } from 'redux/reducers/claims';
|
export { claimsReducer } from 'redux/reducers/claims';
|
||||||
export { fileReducer } from 'redux/reducers/file';
|
|
||||||
export { fileInfoReducer } from 'redux/reducers/file_info';
|
|
||||||
export { notificationsReducer } from 'redux/reducers/notifications';
|
|
||||||
export { searchReducer } from 'redux/reducers/search';
|
|
||||||
export { walletReducer } from 'redux/reducers/wallet';
|
|
||||||
export { contentReducer } from 'redux/reducers/content';
|
|
||||||
export { commentReducer } from 'redux/reducers/comments';
|
export { commentReducer } from 'redux/reducers/comments';
|
||||||
|
export { contentReducer } from 'redux/reducers/content';
|
||||||
|
export { fileInfoReducer } from 'redux/reducers/file_info';
|
||||||
|
export { fileReducer } from 'redux/reducers/file';
|
||||||
|
export { notificationsReducer } from 'redux/reducers/notifications';
|
||||||
|
export { publishReducer } from 'redux/reducers/publish';
|
||||||
|
export { searchReducer } from 'redux/reducers/search';
|
||||||
export { tagsReducerBuilder } from 'redux/reducers/tags';
|
export { tagsReducerBuilder } from 'redux/reducers/tags';
|
||||||
|
export { walletReducer } from 'redux/reducers/wallet';
|
||||||
|
|
||||||
// selectors
|
// selectors
|
||||||
export { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
export { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
||||||
|
@ -193,6 +208,14 @@ export {
|
||||||
selectDownloadedUris,
|
selectDownloadedUris,
|
||||||
} from 'redux/selectors/file_info';
|
} from 'redux/selectors/file_info';
|
||||||
|
|
||||||
|
export {
|
||||||
|
selectPublishFormValues,
|
||||||
|
selectIsStillEditing,
|
||||||
|
selectMyClaimForUri,
|
||||||
|
selectIsResolvingPublishUris,
|
||||||
|
selectTakeOverAmount,
|
||||||
|
} from 'redux/selectors/publish';
|
||||||
|
|
||||||
export { selectSearchState };
|
export { selectSearchState };
|
||||||
export {
|
export {
|
||||||
makeSelectSearchUris,
|
makeSelectSearchUris,
|
||||||
|
|
404
src/redux/actions/publish.js
Normal file
404
src/redux/actions/publish.js
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
// @flow
|
||||||
|
import { CC_LICENSES, COPYRIGHT, OTHER, NONE, PUBLIC_DOMAIN } from 'constants/licenses';
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
//import * as MODALS from 'constants/modal_types';
|
||||||
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
|
import Lbry from 'lbry';
|
||||||
|
import { batchActions } from 'util/batchActions';
|
||||||
|
import { creditsToString } from 'util/formatCredits';
|
||||||
|
import { doError } from 'redux/actions/notifications';
|
||||||
|
import { isClaimNsfw } from 'util/claim';
|
||||||
|
import {
|
||||||
|
selectMyChannelClaims,
|
||||||
|
selectPendingById,
|
||||||
|
selectMyClaimsWithoutChannels,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
|
import { formatLbryUriForWeb } from 'util/uri';
|
||||||
|
|
||||||
|
export const doResetThumbnailStatus = () => (dispatch: Dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: {
|
||||||
|
thumbnailPath: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetch('https://spee.ch/api/config/site/publishing')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(status => {
|
||||||
|
if (status.disabled) {
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: {
|
||||||
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
|
||||||
|
thumbnail: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: {
|
||||||
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN,
|
||||||
|
thumbnail: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doClearPublish = () => (dispatch: Dispatch) => {
|
||||||
|
dispatch({ type: ACTIONS.CLEAR_PUBLISH });
|
||||||
|
return dispatch(doResetThumbnailStatus());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (dispatch: Dispatch) =>
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: { ...publishFormValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const doUploadThumbnail = (filePath: string, thumbnailBuffer: Uint8Array, fsAdapter: any) => (dispatch: Dispatch) => {
|
||||||
|
let thumbnail, fileExt, fileName, fileType;
|
||||||
|
|
||||||
|
const makeid = () => {
|
||||||
|
let text = '';
|
||||||
|
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadError = (error = '') => {
|
||||||
|
dispatch(
|
||||||
|
batchActions(
|
||||||
|
{
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: {
|
||||||
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
|
||||||
|
thumbnail: '',
|
||||||
|
nsfw: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
doError(error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fsAdapter && fsAdapter.readFile && filePath) {
|
||||||
|
fsAdapter.readFile(filePath, 'base64').then(base64Image => {
|
||||||
|
fileExt = 'png';
|
||||||
|
fileName = 'thumbnail.png';
|
||||||
|
fileType = 'image/png';
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
const name = makeid();
|
||||||
|
data.append('name', name);
|
||||||
|
data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName });
|
||||||
|
|
||||||
|
return fetch('https://spee.ch/api/claim/publish', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
}).then(response => response.json())
|
||||||
|
.then(json => json.success
|
||||||
|
? dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: {
|
||||||
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
|
||||||
|
thumbnail: `${json.data.url}${fileExt}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: uploadError(json.message)
|
||||||
|
)
|
||||||
|
.catch(err => uploadError(err.message));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (filePath) {
|
||||||
|
thumbnail = fs.readFileSync(filePath);
|
||||||
|
fileExt = path.extname(filePath);
|
||||||
|
fileName = path.basename(filePath);
|
||||||
|
fileType = `image/${fileExt.slice(1)}`;
|
||||||
|
} else if (thumbnailBuffer) {
|
||||||
|
thumbnail = thumbnailBuffer;
|
||||||
|
fileExt = '.png';
|
||||||
|
fileName = 'thumbnail.png';
|
||||||
|
fileType = 'image/png';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
const name = makeid();
|
||||||
|
const file = new File([thumbnail], fileName, { type: fileType });
|
||||||
|
data.append('name', name);
|
||||||
|
data.append('file', file);
|
||||||
|
|
||||||
|
return fetch('https://spee.ch/api/claim/publish', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json =>
|
||||||
|
json.success
|
||||||
|
? dispatch({
|
||||||
|
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||||
|
data: {
|
||||||
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
|
||||||
|
thumbnail: `${json.data.url}${fileExt}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: uploadError(json.message)
|
||||||
|
)
|
||||||
|
.catch(err => uploadError(err.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem) => (dispatch: Dispatch) => {
|
||||||
|
const { name, amount, channel_name: channelName, value } = claim;
|
||||||
|
|
||||||
|
const {
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
// use same values as default state
|
||||||
|
// fee will be undefined for free content
|
||||||
|
fee = {
|
||||||
|
amount: 0,
|
||||||
|
currency: 'LBC',
|
||||||
|
},
|
||||||
|
languages,
|
||||||
|
license,
|
||||||
|
license_url: licenseUrl,
|
||||||
|
thumbnail,
|
||||||
|
title,
|
||||||
|
} = value;
|
||||||
|
|
||||||
|
const publishData: UpdatePublishFormData = {
|
||||||
|
name,
|
||||||
|
channel: channelName,
|
||||||
|
bid: amount,
|
||||||
|
contentIsFree: !fee.amount,
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
fee: { amount: fee.amount, currency: fee.currency },
|
||||||
|
languages,
|
||||||
|
thumbnail: thumbnail ? thumbnail.url : null,
|
||||||
|
title,
|
||||||
|
uri,
|
||||||
|
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
|
||||||
|
licenseUrl,
|
||||||
|
nsfw: isClaimNsfw(claim),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure custom liscence's are mapped properly
|
||||||
|
// If the license isn't one of the standard licenses, map the custom license and description/url
|
||||||
|
if (!CC_LICENSES.some(({ value }) => value === license)) {
|
||||||
|
if (!license || license === NONE || license === PUBLIC_DOMAIN) {
|
||||||
|
publishData.licenseType = license;
|
||||||
|
} else if (license && !licenseUrl && license !== NONE) {
|
||||||
|
publishData.licenseType = COPYRIGHT;
|
||||||
|
} else {
|
||||||
|
publishData.licenseType = OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishData.otherLicenseDescription = license;
|
||||||
|
} else {
|
||||||
|
publishData.licenseType = license;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo && fileInfo.download_path) {
|
||||||
|
try {
|
||||||
|
fs.accessSync(fileInfo.download_path, fs.constants.R_OK);
|
||||||
|
publishData.filePath = fileInfo.download_path;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.name, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
So we expect every actionListener to call doUpdatePublishForm and I add the selector into the doPublish action, I assume. So we expect every actionListener to call doUpdatePublishForm and I add the selector into the doPublish action, I assume.
I also had to add makeSelectPublishFormValue item => ... for desktop not to crash.
|
|||||||
|
dispatch({ type: ACTIONS.PUBLISH_START });
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const myChannels = selectMyChannelClaims(state);
|
||||||
|
const myClaims = selectMyClaimsWithoutChannels(state);
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
bid,
|
||||||
|
filePath,
|
||||||
|
description,
|
||||||
|
language,
|
||||||
|
license,
|
||||||
|
licenseUrl,
|
||||||
|
thumbnail,
|
||||||
|
channel,
|
||||||
|
title,
|
||||||
|
contentIsFree,
|
||||||
|
fee,
|
||||||
|
uri,
|
||||||
|
nsfw,
|
||||||
|
claim,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
// get the claim id from the channel name, we will use that instead
|
||||||
|
const namedChannelClaim = myChannels.find(myChannel => myChannel.name === channel);
|
||||||
|
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
||||||
|
|
||||||
|
const publishPayload: {
|
||||||
|
name: ?string,
|
||||||
|
channel_id?: string,
|
||||||
|
bid: number,
|
||||||
|
file_path?: string,
|
||||||
|
tags: Array<string>,
|
||||||
|
locations?: Array<Location>,
|
||||||
|
license_url?: string,
|
||||||
|
thumbnail_url?: string,
|
||||||
|
release_time?: number,
|
||||||
|
fee_currency?: string,
|
||||||
|
fee_amount?: string,
|
||||||
|
} = {
|
||||||
|
name,
|
||||||
|
bid: creditsToString(bid),
|
||||||
|
title,
|
||||||
|
license,
|
||||||
|
languages: [language],
|
||||||
|
description,
|
||||||
|
tags: (claim && claim.value.tags) || [],
|
||||||
|
locations: claim && claim.value.locations,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporary solution to keep the same publish flow with the new tags api
|
||||||
|
// Eventually we will allow users to enter their own tags on publish
|
||||||
|
// `nsfw` will probably be removed
|
||||||
|
|
||||||
|
if (licenseUrl) {
|
||||||
|
publishPayload.license_url = licenseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
publishPayload.thumbnail_url = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim && claim.value.release_time) {
|
||||||
|
publishPayload.release_time = Number(claim.value.release_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nsfw) {
|
||||||
|
if (!publishPayload.tags.includes('mature')) {
|
||||||
|
publishPayload.tags.push('mature');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const indexToRemove = publishPayload.tags.indexOf('mature');
|
||||||
|
if (indexToRemove > -1) {
|
||||||
|
publishPayload.tags.splice(indexToRemove, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelId) {
|
||||||
|
publishPayload.channel_id = channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contentIsFree && fee && (fee.currency && Number(fee.amount) > 0)) {
|
||||||
|
publishPayload.fee_currency = fee.currency;
|
||||||
|
publishPayload.fee_amount = creditsToString(fee.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pass file on new uploads, not metadata only edits.
|
||||||
|
// The sdk will figure it out
|
||||||
|
if (filePath) publishPayload.file_path = filePath;
|
||||||
|
|
||||||
|
const success = successResponse => {
|
||||||
|
//analytics.apiLogPublish();
|
||||||
|
|
||||||
|
const pendingClaim = successResponse.outputs[0];
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: ACTIONS.PUBLISH_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
//actions.push(doOpenModal(MODALS.PUBLISH, { uri }));
|
||||||
|
|
||||||
|
// We have to fake a temp claim until the new pending one is returned by claim_list_mine
|
||||||
|
// We can't rely on claim_list_mine because there might be some delay before the new claims are returned
|
||||||
|
// Doing this allows us to show the pending claim immediately, it will get overwritten by the real one
|
||||||
|
const isMatch = claim => claim.claim_id === pendingClaim.claim_id;
|
||||||
|
const isEdit = myClaims.some(isMatch);
|
||||||
|
const myNewClaims = isEdit
|
||||||
|
? myClaims.map(claim => (isMatch(claim) ? pendingClaim : claim))
|
||||||
|
: myClaims.concat(pendingClaim);
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
|
||||||
|
data: {
|
||||||
|
claims: myNewClaims,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(batchActions(...actions));
|
||||||
|
};
|
||||||
|
|
||||||
|
const failure = error => {
|
||||||
|
dispatch({ type: ACTIONS.PUBLISH_FAIL });
|
||||||
|
dispatch(doError(error.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
return Lbry.publish(publishPayload).then(success, failure);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calls claim_list_mine until any pending publishes are confirmed
|
||||||
|
export const doCheckPendingPublishes = () => (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const pendingById = selectPendingById(state);
|
||||||
|
|
||||||
|
if (!Object.keys(pendingById).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let publishCheckInterval;
|
||||||
|
|
||||||
|
const checkFileList = () => {
|
||||||
|
Lbry.claim_list().then(claims => {
|
||||||
|
claims.forEach(claim => {
|
||||||
|
// If it's confirmed, check if it was pending previously
|
||||||
|
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
|
||||||
|
delete pendingById[claim.claim_id];
|
||||||
|
|
||||||
|
// If it's confirmed, check if we should notify the user
|
||||||
|
if (selectosNotificationsEnabled(getState())) {
|
||||||
|
const notif = new window.Notification('LBRY Publish Complete', {
|
||||||
|
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
|
||||||
|
silent: false,
|
||||||
|
});
|
||||||
|
notif.onclick = () => {
|
||||||
|
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
|
||||||
|
data: {
|
||||||
|
claims,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Object.keys(pendingById).length) {
|
||||||
|
clearInterval(publishCheckInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
publishCheckInterval = setInterval(() => {
|
||||||
|
checkFileList();
|
||||||
|
}, 30000);
|
||||||
|
};
|
107
src/redux/reducers/publish.js
Normal file
107
src/redux/reducers/publish.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// @flow
|
||||||
|
import { handleActions } from 'util/redux-utils';
|
||||||
|
import { buildURI } from 'lbryURI';
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
|
import { CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||||
|
|
||||||
|
type PublishState = {
|
||||||
|
editingURI: ?string,
|
||||||
|
filePath: ?string,
|
||||||
|
contentIsFree: boolean,
|
||||||
|
fee: {
|
||||||
|
amount: number,
|
||||||
|
currency: string,
|
||||||
|
},
|
||||||
|
title: string,
|
||||||
|
thumbnail_url: string,
|
||||||
|
thumbnailPath: string,
|
||||||
|
uploadThumbnailStatus: string,
|
||||||
|
description: string,
|
||||||
|
language: string,
|
||||||
|
channel: string,
|
||||||
|
channelId: ?string,
|
||||||
|
name: string,
|
||||||
|
nameError: ?string,
|
||||||
|
bid: number,
|
||||||
|
bidError: ?string,
|
||||||
|
otherLicenseDescription: string,
|
||||||
|
licenseUrl: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState: PublishState = {
|
||||||
|
editingURI: undefined,
|
||||||
|
filePath: undefined,
|
||||||
|
contentIsFree: true,
|
||||||
|
fee: {
|
||||||
|
amount: 1,
|
||||||
|
currency: 'LBC',
|
||||||
|
},
|
||||||
|
title: '',
|
||||||
|
thumbnail_url: '',
|
||||||
|
thumbnailPath: '',
|
||||||
|
uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN,
|
||||||
|
description: '',
|
||||||
|
language: 'en',
|
||||||
|
nsfw: false,
|
||||||
|
channel: CHANNEL_ANONYMOUS,
|
||||||
|
channelId: '',
|
||||||
|
name: '',
|
||||||
|
nameError: undefined,
|
||||||
|
bid: 0.1,
|
||||||
|
bidError: undefined,
|
||||||
|
licenseType: 'None',
|
||||||
|
otherLicenseDescription: 'All rights reserved',
|
||||||
|
licenseUrl: '',
|
||||||
|
publishing: false,
|
||||||
|
publishSuccess: false,
|
||||||
|
publishError: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishReducer = handleActions(
|
||||||
|
{
|
||||||
|
[ACTIONS.UPDATE_PUBLISH_FORM]: (state, action): PublishState => {
|
||||||
|
const { data } = action;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.CLEAR_PUBLISH]: (): PublishState => ({
|
||||||
|
...defaultState,
|
||||||
|
}),
|
||||||
|
[ACTIONS.PUBLISH_START]: (state: PublishState): PublishState => ({
|
||||||
|
...state,
|
||||||
|
publishing: true,
|
||||||
|
publishSuccess: false,
|
||||||
|
}),
|
||||||
|
[ACTIONS.PUBLISH_FAIL]: (state: PublishState): PublishState => ({
|
||||||
|
...state,
|
||||||
|
publishing: false,
|
||||||
|
}),
|
||||||
|
[ACTIONS.PUBLISH_SUCCESS]: (state: PublishState): PublishState => ({
|
||||||
|
...state,
|
||||||
|
publishing: false,
|
||||||
|
publishSuccess: true,
|
||||||
|
}),
|
||||||
|
[ACTIONS.DO_PREPARE_EDIT]: (state: PublishState, action) => {
|
||||||
|
const { ...publishData } = action.data;
|
||||||
|
const { channel, name, uri } = publishData;
|
||||||
|
|
||||||
|
// The short uri is what is presented to the user
|
||||||
|
// The editingUri is the full uri with claim id
|
||||||
|
const shortUri = buildURI({
|
||||||
|
channelName: channel,
|
||||||
|
contentName: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultState,
|
||||||
|
...publishData,
|
||||||
|
editingURI: uri,
|
||||||
|
uri: shortUri,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultState
|
||||||
|
);
|
105
src/redux/selectors/publish.js
Normal file
105
src/redux/selectors/publish.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { buildURI, parseURI } from 'lbryURI';
|
||||||
|
import {
|
||||||
|
selectClaimsById,
|
||||||
|
selectMyClaimsWithoutChannels,
|
||||||
|
selectResolvingUris,
|
||||||
|
selectClaimsByUri,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
|
|
||||||
|
const selectState = state => state.publish || {};
|
||||||
|
|
||||||
|
export const selectPublishFormValues = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => {
|
||||||
|
const { pendingPublish, ...formValues } = state;
|
||||||
|
return formValues;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Is the current uri the same as the uri they clicked "edit" on
|
||||||
|
export const selectIsStillEditing = createSelector(
|
||||||
|
selectPublishFormValues,
|
||||||
|
publishState => {
|
||||||
|
const { editingURI, uri } = publishState;
|
||||||
|
|
||||||
|
if (!editingURI || !uri) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri);
|
||||||
|
const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI);
|
||||||
|
|
||||||
|
// Depending on the previous/current use of a channel, we need to compare different things
|
||||||
|
// ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName
|
||||||
|
const currentName = currentIsChannel ? currentContentName : currentClaimName;
|
||||||
|
const editName = editIsChannel ? editContentName : editClaimName;
|
||||||
|
return currentName === editName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectMyClaimForUri = createSelector(
|
||||||
|
selectPublishFormValues,
|
||||||
|
selectIsStillEditing,
|
||||||
|
selectClaimsById,
|
||||||
|
selectMyClaimsWithoutChannels,
|
||||||
|
({ editingURI, uri }, isStillEditing, claimsById, myClaims) => {
|
||||||
|
const { contentName, claimName } = parseURI(uri);
|
||||||
|
const { claimId: editClaimId } = parseURI(editingURI);
|
||||||
|
|
||||||
|
// If isStillEditing
|
||||||
|
// They clicked "edit" from the file page
|
||||||
|
// They haven't changed the channel/name after clicking edit
|
||||||
|
// Get the claim so they can edit without re-uploading a new file
|
||||||
|
return isStillEditing
|
||||||
|
? claimsById[editClaimId]
|
||||||
|
: myClaims.find(claim =>
|
||||||
|
!contentName ? claim.name === claimName : claim.name === contentName || claim.name === claimName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsResolvingPublishUris = createSelector(
|
||||||
|
selectState,
|
||||||
|
selectResolvingUris,
|
||||||
|
({ uri, name }, resolvingUris) => {
|
||||||
|
if (uri) {
|
||||||
|
const isResolvingUri = resolvingUris.includes(uri);
|
||||||
|
const { isChannel } = parseURI(uri);
|
||||||
|
|
||||||
|
let isResolvingShortUri;
|
||||||
|
if (isChannel) {
|
||||||
|
const shortUri = buildURI({ contentName: name });
|
||||||
|
isResolvingShortUri = resolvingUris.includes(shortUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isResolvingUri || isResolvingShortUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectTakeOverAmount = createSelector(
|
||||||
|
selectState,
|
||||||
|
selectMyClaimForUri,
|
||||||
|
selectClaimsByUri,
|
||||||
|
({ name }, myClaimForUri, claimsByUri) => {
|
||||||
|
// We only care about the winning claim for the short uri
|
||||||
|
const shortUri = buildURI({ contentName: name });
|
||||||
|
const claimForShortUri = claimsByUri[shortUri];
|
||||||
|
|
||||||
|
if (!myClaimForUri && claimForShortUri) {
|
||||||
|
return claimForShortUri.effective_amount;
|
||||||
|
} else if (myClaimForUri && claimForShortUri) {
|
||||||
|
// https://github.com/lbryio/lbry/issues/1476
|
||||||
|
// We should check the current effective_amount on my claim to see how much additional lbc
|
||||||
|
// is needed to win the claim. Currently this is not possible during a takeover.
|
||||||
|
// With this, we could say something like, "You have x lbc in support, if you bid y additional LBC you will control the claim"
|
||||||
|
// For now just ignore supports. We will just show the winning claim's bid amount
|
||||||
|
return claimForShortUri.effective_amount || claimForShortUri.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
13
src/util/uri.js
Normal file
13
src/util/uri.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// @flow
|
||||||
|
import { parseURI } from 'lbryURI';
|
||||||
|
|
||||||
|
export const formatLbryUriForWeb = (uri: string) => {
|
||||||
|
const { claimName, claimId } = parseURI(uri);
|
||||||
|
|
||||||
|
let webUrl = `/${claimName}`;
|
||||||
|
if (claimId) {
|
||||||
|
webUrl += `/${claimId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return webUrl;
|
||||||
|
};
|
Loading…
Reference in a new issue
Can this be updated to use the new logic in
doPublish
for the desktop app?It removes the need to pass all of the values to
doPublish
since we already store them in state.https://github.com/lbryio/lbry-desktop/blob/master/src/ui/redux/actions/publish.js#L208-L250
@jessopb maybe you could do this?
@akinwale it will require some app changes, but it's a lot nicer to work with.