Publishing #158

Merged
akinwale merged 5 commits from publishing into master 2019-07-01 21:49:51 +02:00
10 changed files with 1949 additions and 592 deletions

1731
dist/bundle.es.js vendored

File diff suppressed because it is too large Load diff

51
dist/flow-typed/Publish.js vendored Normal file
View 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
View 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
View 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
View 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';

View file

@ -1,9 +1,11 @@
import * as CLAIM_VALUES from 'constants/claim';
import * as ACTIONS from 'constants/action_types';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import * as SETTINGS from 'constants/settings';
import * as TRANSACTIONS from 'constants/transaction_types';
import * as SORT_OPTIONS from 'constants/sort_options';
import * as LICENSES from 'constants/licenses';
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 Lbry from 'lbry';
import { selectState as selectSearchState } from 'redux/selectors/search';
@ -11,6 +13,8 @@ import { selectState as selectSearchState } from 'redux/selectors/search';
// constants
export {
ACTIONS,
CLAIM_VALUES,
LICENSES,
THUMBNAIL_STATUSES,
SEARCH_TYPES,
SEARCH_OPTIONS,
@ -57,6 +61,16 @@ export {
doSetFileListSort,
} from 'redux/actions/file_info';
export {
doResetThumbnailStatus,
doClearPublish,
doUpdatePublishForm,
doUploadThumbnail,
doPrepareEdit,
doPublish,
doCheckPendingPublishes
} from 'redux/actions/publish';
export {
doSearch,
doUpdateSearchQuery,
@ -100,14 +114,15 @@ export { isClaimNsfw } from 'util/claim';
// reducers
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 { 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 { walletReducer } from 'redux/reducers/wallet';
// selectors
export { makeSelectContentPositionForUri } from 'redux/selectors/content';
@ -193,6 +208,14 @@ export {
selectDownloadedUris,
} from 'redux/selectors/file_info';
export {
selectPublishFormValues,
selectIsStillEditing,
selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
} from 'redux/selectors/publish';
export { selectSearchState };
export {
makeSelectSearchUris,

View 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: () => {}) => {
neb-b commented 2019-07-02 22:27:40 +02:00 (Migrated from github.com)
Review

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.

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.
jessopb commented 2019-07-02 22:37:25 +02:00 (Migrated from github.com)
Review

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.

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);
};

View 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
);

View 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
View 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;
};