Publishing (#158)
* add actions, selectors and reducer for publishing * add publish types * support thumbnail uploads for mobile * update publishSuccess value in reducer
This commit is contained in:
parent
1415935006
commit
99e08fc733
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: () => {}) => {
|
||||||
|
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