From f5289f981145b5fb2867f4e369ef3ad9f3e913a6 Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Tue, 20 Aug 2019 16:00:26 -0400 Subject: [PATCH 1/2] use canonical_url everywhere and update url parse/build functions to work with canonical_url's properly --- .flowconfig | 1 + dist/bundle.es.js | 364 ++++++++++++++++++++------------- dist/flow-typed/Claim.js | 1 + dist/flow-typed/Lbry.js | 2 +- dist/flow-typed/i18n.js | 2 + flow-typed/Claim.js | 1 + flow-typed/Lbry.js | 2 +- flow-typed/i18n.js | 2 + src/index.js | 1 + src/lbryURI.js | 341 ++++++++++++++++++------------ src/redux/actions/claims.js | 8 +- src/redux/actions/search.js | 24 ++- src/redux/reducers/claims.js | 33 ++- src/redux/selectors/claims.js | 38 ++-- src/redux/selectors/publish.js | 18 +- src/redux/selectors/search.js | 10 +- src/util/uri.js | 13 -- 17 files changed, 515 insertions(+), 346 deletions(-) create mode 100644 dist/flow-typed/i18n.js create mode 100644 flow-typed/i18n.js delete mode 100644 src/util/uri.js diff --git a/.flowconfig b/.flowconfig index aca52ab..64516a4 100644 --- a/.flowconfig +++ b/.flowconfig @@ -6,6 +6,7 @@ ./flow-typed [options] +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe module.system.node.resolve_dirname=./src module.name_mapper='^redux\(.*\)$' -> '/src/redux\1' module.name_mapper='^util\(.*\)$' -> '/src/util\1' diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 6ae34e2..45fb468 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -897,45 +897,49 @@ const getSearchQueryString = (query, options = {}, includeUserOptions = false) = var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + +// +const isProduction = process.env.NODE_ENV === 'production'; const channelNameMinLength = 1; const claimIdMaxLength = 40; // see https://spec.lbry.com/#urls const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u; const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/; +const regexPartProtocol = '^((?:lbry://)?)'; +const regexPartStreamOrChannelName = '([^:$#/]*)'; +const regexPartModifierSeparator = '([:$#]?)([^/]*)'; /** * Parses a LBRY name into its component parts. Throws errors with user-friendly * messages for invalid names. * - * N.B. that "name" indicates the value in the name position of the URI. For - * claims for channel content, this will actually be the channel name, and - * the content name is in the path (e.g. lbry://@channel/content) - * - * In most situations, you'll want to use the contentName and channelName keys - * and ignore the name key. - * * Returns a dictionary with keys: - * - name (string): The value in the "name" position in the URI. Note that this - * could be either content name or channel name; see above. - * - path (string, if persent) - * - claimSequence (int, if present) - * - bidPosition (int, if present) - * - claimId (string, if present) + * - path (string) * - isChannel (boolean) - * - contentName (string): For anon claims, the name; for channel claims, the path - * - channelName (string, if present): Channel name without @ + * - streamName (string, if present) + * - streamClaimId (string, if present) + * - channelName (string, if present) + * - channelClaimId (string, if present) + * - primaryClaimSequence (int, if present) + * - secondaryClaimSequence (int, if present) + * - primaryBidPosition (int, if present) + * - secondaryBidPosition (int, if present) */ -function parseURI(URI, requireProto = false) { - // Break into components. Empty sub-matches are converted to null - const componentsRegex = new RegExp('^((?:lbry://)?)' + // protocol - '([^:$#/]*)' + // claim name (stops at the first separator or end) - '([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) - '(/?)(.*)' // path separator, path - ); - const [proto, claimName, modSep, modVal, pathSep, path] = componentsRegex.exec(URI).slice(1).map(match => match || null); - let contentName; +function parseURI(URL, requireProto = false) { + // Break into components. Empty sub-matches are converted to null + const componentsRegex = new RegExp(regexPartProtocol + // protocol + regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end) + regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end) + '(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts + regexPartStreamOrChannelName + regexPartModifierSeparator); + + const regexMatch = componentsRegex.exec(URL) || []; + const [proto, ...rest] = regexMatch.slice(1).map(match => match || null); + const path = rest.join(''); + const [streamNameOrChannelName, primaryModSeparator, primaryModValue, pathSep, possibleStreamName, secondaryModSeparator, secondaryModValue] = rest; // Validate protocol if (requireProto && !proto) { @@ -943,14 +947,15 @@ function parseURI(URI, requireProto = false) { } // Validate and process name - if (!claimName) { + if (!streamNameOrChannelName) { throw new Error(__('URI does not include name.')); } - const isChannel = claimName.startsWith('@'); - const channelName = isChannel ? claimName.slice(1) : claimName; + const includesChannel = streamNameOrChannelName.startsWith('@'); + const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName; + const channelName = includesChannel && streamNameOrChannelName.slice(1); - if (isChannel) { + if (includesChannel) { if (!channelName) { throw new Error(__('No channel name after @.')); } @@ -958,30 +963,42 @@ function parseURI(URI, requireProto = false) { if (channelName.length < channelNameMinLength) { throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength)); } - - contentName = path; } - const nameBadChars = (channelName || claimName).match(regexInvalidURI); - if (nameBadChars) { - throw new Error(__(`Invalid character %s in name: %s.`, nameBadChars.length === 1 ? '' : 's', nameBadChars.join(', '))); - } + // Validate and process modifier + const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(primaryModSeparator, primaryModValue); + const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(secondaryModSeparator, secondaryModValue); + const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName; + const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId; + const channelClaimId = includesChannel && primaryClaimId; - // Validate and process modifier (claim ID, bid position or claim sequence) + return _extends({ + isChannel, + path + }, streamName ? { streamName } : {}, streamClaimId ? { streamClaimId } : {}, channelName ? { channelName } : {}, channelClaimId ? { channelClaimId } : {}, primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}, secondaryClaimSequence ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } : {}, primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}, secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}, { + + // The values below should not be used for new uses of parseURI + // They will not work properly with canonical_urls + claimName: streamNameOrChannelName, + claimId: primaryClaimId + }, streamName ? { contentName: streamName } : {}); +} + +function parseURIModifier(modSeperator, modValue) { let claimId; let claimSequence; let bidPosition; - if (modSep) { - if (!modVal) { - throw new Error(__(`No modifier provided after separator %s.`, modSep)); + if (modSeperator) { + if (!modValue) { + throw new Error(__(`No modifier provided after separator %s.`, modSeperator)); } - if (modSep === '#') { - claimId = modVal; - } else if (modSep === ':') { - claimSequence = modVal; - } else if (modSep === '$') { - bidPosition = modVal; + if (modSeperator === '#') { + claimId = modValue; + } else if (modSeperator === ':') { + claimSequence = modValue; + } else if (modSeperator === '$') { + bidPosition = modValue; } } @@ -997,27 +1014,7 @@ function parseURI(URI, requireProto = false) { throw new Error(__('Bid position must be a number.')); } - // Validate and process path - if (path) { - if (!isChannel) { - throw new Error(__('Only channel URIs may have a path.')); - } - - const pathBadChars = path.match(regexInvalidURI); - if (pathBadChars) { - throw new Error(__(`Invalid character in path: %s`, pathBadChars.join(', '))); - } - - contentName = path; - } else if (pathSep) { - throw new Error(__('No path provided after /')); - } - - return _extends({ - claimName, - path, - isChannel - }, contentName ? { contentName } : {}, channelName ? { channelName } : {}, claimSequence ? { claimSequence: parseInt(claimSequence, 10) } : {}, bidPosition ? { bidPosition: parseInt(bidPosition, 10) } : {}, claimId ? { claimId } : {}, path ? { path } : {}); + return [claimId, claimSequence, bidPosition]; } /** @@ -1025,67 +1022,119 @@ function parseURI(URI, requireProto = false) { * * The channelName key will accept names with or without the @ prefix. */ -function buildURI(URIObj, includeProto = true, protoDefault = 'lbry://') { - const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj; +function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition + } = UrlObj, + deprecatedParts = _objectWithoutProperties(UrlObj, ['streamName', 'streamClaimId', 'channelName', 'channelClaimId', 'primaryClaimSequence', 'primaryBidPosition', 'secondaryClaimSequence', 'secondaryBidPosition']); + const { claimId, claimName, contentName } = deprecatedParts; - let { claimName, path } = URIObj; - - if (channelName) { - const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`; - if (!claimName) { - claimName = channelNameFormatted; - } else if (claimName !== channelNameFormatted) { - throw new Error(__('Received a channel content URI, but claim name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.')); + if (!isProduction) { + if (claimId) { + console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")); + } + if (claimName) { + console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead")); + } + if (contentName) { + console.error(__("'contentName' should no longer be used. Use 'streamName' instead")); } } - if (contentName) { - if (!claimName) { - claimName = contentName; - } else if (!path) { - path = contentName; - } - if (path && path !== contentName) { - throw new Error(__('Path and contentName do not match. Only one is required; most likely you wanted contentName.')); - } + if (!claimName && !channelName && !streamName) { + throw new Error(__("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.")); } - return (includeProto ? protoDefault : '') + claimName + (claimId ? `#${claimId}` : '') + (claimSequence ? `:${claimSequence}` : '') + (bidPosition ? `${bidPosition}` : '') + (path ? `/${path}` : ''); + const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`); + const primaryClaimName = claimName || formattedChannelName || streamName; + const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId); + const secondaryClaimName = !claimName && (formattedChannelName ? streamName : null); + const secondaryClaimId = secondaryClaimName && streamClaimId; + + return (includeProto ? protoDefault : '') + + // primaryClaimName will always exist here because we throw above if there is no "name" value passed in + // $FlowFixMe + primaryClaimName + (primaryClaimId ? `#${primaryClaimId}` : '') + (primaryClaimSequence ? `:${primaryClaimSequence}` : '') + (primaryBidPosition ? `${primaryBidPosition}` : '') + (secondaryClaimName ? `/${secondaryClaimName}` : '') + (secondaryClaimId ? `#${secondaryClaimId}` : '') + (secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') + (secondaryBidPosition ? `${secondaryBidPosition}` : ''); } -/* Takes a parseable LBRY URI and converts it to standard, canonical format */ -function normalizeURI(URI) { - const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); - return buildURI({ claimName, path, claimSequence, bidPosition, claimId }); +/* Takes a parseable LBRY URL and converts it to standard, canonical format */ +function normalizeURI(URL) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition + } = parseURI(URL); + + return buildURI({ + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition + }); } -function isURIValid(URI) { - let parts; +function isURIValid(URL) { try { - parts = parseURI(normalizeURI(URI)); + parseURI(normalizeURI(URL)); } catch (error) { return false; } - return parts && parts.claimName; + + return true; } function isNameValid(claimName) { return !regexInvalidURI.test(claimName); } -function isURIClaimable(URI) { +function isURIClaimable(URL) { let parts; try { - parts = parseURI(normalizeURI(URI)); + parts = parseURI(normalizeURI(URL)); } catch (error) { return false; } - return parts && parts.claimName && !parts.claimId && !parts.bidPosition && !parts.claimSequence && !parts.isChannel && !parts.path; + + return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel; } -function convertToShareLink(URI) { - const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); - return buildURI({ claimName, path, claimSequence, bidPosition, claimId }, true, 'https://open.lbry.com/'); +function convertToShareLink(URL) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence + } = parseURI(URL); + return buildURI({ + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence + }, true, 'https://open.lbry.com/'); } var _extends$1 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; @@ -1130,13 +1179,13 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec let searchSuggestions = []; try { const uri = normalizeURI(query); - const { claimName, isChannel } = parseURI(uri); + const { channelName, streamName, isChannel } = parseURI(uri); searchSuggestions.push({ - value: claimName, + value: streamName, type: SEARCH_TYPES.SEARCH }, { value: uri, - shorthand: isChannel ? claimName.slice(1) : claimName, + shorthand: isChannel ? channelName : streamName, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE }); } catch (e) { @@ -1157,11 +1206,11 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec // determine if it's a channel try { const uri = normalizeURI(suggestion); - const { claimName, isChannel } = parseURI(uri); + const { channelName, streamName, isChannel } = parseURI(uri); return { value: uri, - shorthand: isChannel ? claimName.slice(1) : claimName, + shorthand: isChannel ? channelName : streamName, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE }; } catch (e) { @@ -1377,7 +1426,7 @@ const selectTransactionListFilter = reselect.createSelector(selectState$1, state var _extends$2 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } +function _objectWithoutProperties$1(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } const matureTagMap = MATURE_TAGS.reduce((acc, tag) => _extends$2({}, acc, { [tag]: true }), {}); @@ -1404,7 +1453,7 @@ const isClaimNsfw = claim => { function createNormalizedClaimSearchKey(options) { // Ignore page because we don't care what the last page searched was, we want everything // Ignore release_time because that will change depending on when you call claim_search ex: release_time: ">12344567" - const rest = _objectWithoutProperties(options, ['page', 'release_time']); + const rest = _objectWithoutProperties$1(options, ['page', 'release_time']); const query = JSON.stringify(rest); return query; } @@ -1445,8 +1494,10 @@ const selectPendingClaims = reselect.createSelector(selectState$2, state => Obje const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingById, pendingById => { let claimId; + try { - ({ claimId } = parseURI(uri)); + const { isChannel, channelClaimId, streamClaimId } = parseURI(uri); + claimId = isChannel ? channelClaimId : streamClaimId; } catch (e) {} if (claimId) { @@ -1455,7 +1506,8 @@ const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingByI }); const makeSelectPendingByUri = uri => reselect.createSelector(selectPendingById, pendingById => { - const { claimId } = parseURI(uri); + const { isChannel, channelClaimId, streamClaimId } = parseURI(uri); + const claimId = isChannel ? channelClaimId : streamClaimId; return pendingById[claimId]; }); @@ -1464,13 +1516,16 @@ const makeSelectClaimForUri = uri => reselect.createSelector(selectClaimsByUri, // It won't be in claimsByUri because resolving it will return nothing let valid; - let claimId; + let channelClaimId; + let streamClaimId; + let isChannel; try { - ({ claimId } = parseURI(uri)); + ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri)); valid = true; } catch (e) {} if (valid) { + const claimId = isChannel ? channelClaimId : streamClaimId; const pendingClaim = pendingById[claimId]; if (pendingClaim) { @@ -1720,15 +1775,18 @@ const selectClaimSearchByQueryLastPageReached = reselect.createSelector(selectSt const makeSelectShortUrlForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), claim => claim && claim.short_url); +const makeSelectCanonicalUrlForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), claim => claim && claim.canonical_url); + const makeSelectSupportsForUri = uri => reselect.createSelector(selectSupportsByOutpoint, makeSelectClaimForUri(uri), (byOutpoint, claim) => { if (!claim || !claim.is_mine) { return null; } const { claim_id: claimId } = claim; - let total = parseFloat("0.0"); + let total = 0; Object.values(byOutpoint).forEach(support => { + // $FlowFixMe const { claim_id, amount } = support; total = claim_id === claimId && amount ? total + parseFloat(amount) : total; }); @@ -2433,10 +2491,10 @@ function doClaimSearch(options = { const success = data => { const resolveInfo = {}; - const uris = []; + const urls = []; data.items.forEach(stream => { - resolveInfo[stream.permanent_url] = { stream }; - uris.push(stream.permanent_url); + resolveInfo[stream.canonical_url] = { stream }; + urls.push(stream.canonical_url); }); dispatch({ @@ -2444,7 +2502,7 @@ function doClaimSearch(options = { data: { query, resolveInfo, - uris, + urls, append: options.page && options.page !== 1, pageSize: options.page_size } @@ -2858,12 +2916,12 @@ function doSetFileListSort(page, value) { }; } -function _objectWithoutProperties$1(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } +function _objectWithoutProperties$2(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } const selectState$5 = state => state.publish || {}; const selectPublishFormValues = reselect.createSelector(selectState$5, state => { - const formValues = _objectWithoutProperties$1(state, ['pendingPublish']); + const formValues = _objectWithoutProperties$2(state, ['pendingPublish']); return formValues; }); const makeSelectPublishFormValue = item => reselect.createSelector(selectState$5, state => state[item]); @@ -2876,8 +2934,16 @@ const selectIsStillEditing = reselect.createSelector(selectPublishFormValues, pu return false; } - const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); - const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); + 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 @@ -2904,7 +2970,7 @@ const selectIsResolvingPublishUris = reselect.createSelector(selectState$5, sele let isResolvingShortUri; if (isChannel) { - const shortUri = buildURI({ contentName: name }); + const shortUri = buildURI({ streamName: name }); isResolvingShortUri = resolvingUris.includes(shortUri); } @@ -3376,13 +3442,21 @@ from, isBackgroundSearch = false) => (dispatch, getState) => { const actions = []; data.forEach(result => { - if (result.name) { - const uri = buildURI({ - claimName: result.name, - claimId: result.claimId - }); - actions.push(doResolveUri(uri)); - uris.push(uri); + if (result) { + const { name, claimId } = result; + const urlObj = {}; + + if (name.startsWith('@')) { + urlObj.channelName = name; + urlObj.channelClaimId = claimId; + } else { + urlObj.streamName = name; + urlObj.streamClaimId = claimId; + } + + const url = buildURI(urlObj); + actions.push(doResolveUri(url)); + uris.push(url); } }); @@ -3561,26 +3635,25 @@ function handleClaimAction(state, action) { const byId = Object.assign({}, state.byId); const channelClaimCounts = Object.assign({}, state.channelClaimCounts); - Object.entries(resolveInfo).forEach(([uri, resolveResponse]) => { + Object.entries(resolveInfo).forEach(([url, resolveResponse]) => { // $FlowFixMe - if (resolveResponse.claimsInChannel) { - // $FlowFixMe - channelClaimCounts[uri] = resolveResponse.claimsInChannel; + const { claimsInChannel, stream, channel } = resolveResponse; + if (claimsInChannel) { + channelClaimCounts[url] = claimsInChannel; } - }); - // $FlowFixMe - Object.entries(resolveInfo).forEach(([uri, { channel, stream }]) => { if (stream) { byId[stream.claim_id] = stream; - byUri[uri] = stream.claim_id; + byUri[url] = stream.claim_id; } + if (channel) { byId[channel.claim_id] = channel; - byUri[stream ? channel.permanent_url : uri] = channel.claim_id; + byUri[stream ? channel.canonical_url : url] = channel.claim_id; } + if (!stream && !channel) { - byUri[uri] = null; + byUri[url] = null; } }); @@ -3792,17 +3865,17 @@ reducers[CLAIM_SEARCH_COMPLETED] = (state, action) => { const fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery); const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery); const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached); - const { append, query, uris, pageSize } = action.data; + const { append, query, urls, pageSize } = action.data; if (append) { - // todo: check for duplicate uris when concatenating? - claimSearchByQuery[query] = claimSearchByQuery[query] && claimSearchByQuery[query].length ? claimSearchByQuery[query].concat(uris) : uris; + // todo: check for duplicate urls when concatenating? + claimSearchByQuery[query] = claimSearchByQuery[query] && claimSearchByQuery[query].length ? claimSearchByQuery[query].concat(urls) : urls; } else { - claimSearchByQuery[query] = uris; + claimSearchByQuery[query] = urls; } - // the returned number of uris is less than the page size, so we're on the last page - claimSearchByQueryLastPageReached[query] = uris.length < pageSize; + // the returned number of urls is less than the page size, so we're on the last page + claimSearchByQueryLastPageReached[query] = urls.length < pageSize; delete fetchingClaimSearchByQuery[query]; @@ -4232,7 +4305,7 @@ const notificationsReducer = handleActions({ var _extends$a = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -function _objectWithoutProperties$2(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } +function _objectWithoutProperties$3(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } const defaultState$6 = { editingURI: undefined, @@ -4282,7 +4355,7 @@ const publishReducer = handleActions({ publishSuccess: true }), [DO_PREPARE_EDIT]: (state, action) => { - const publishData = _objectWithoutProperties$2(action.data, []); + const publishData = _objectWithoutProperties$3(action.data, []); const { channel, name, uri } = publishData; // The short uri is what is presented to the user @@ -4892,6 +4965,7 @@ exports.isNameValid = isNameValid; exports.isURIClaimable = isURIClaimable; exports.isURIValid = isURIValid; exports.makeSelectAmountForUri = makeSelectAmountForUri; +exports.makeSelectCanonicalUrlForUri = makeSelectCanonicalUrlForUri; exports.makeSelectChannelForClaimUri = makeSelectChannelForClaimUri; exports.makeSelectClaimForUri = makeSelectClaimForUri; exports.makeSelectClaimIsMine = makeSelectClaimIsMine; diff --git a/dist/flow-typed/Claim.js b/dist/flow-typed/Claim.js index 1c61c10..54e414f 100644 --- a/dist/flow-typed/Claim.js +++ b/dist/flow-typed/Claim.js @@ -23,6 +23,7 @@ declare type GenericClaim = { decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 timestamp?: number, // date of last transaction height: number, // block height the tx was confirmed + is_mine: boolean, name: string, normalized_name: string, // `name` normalized via unicode NFD spec, nout: number, // index number for an output of a tx diff --git a/dist/flow-typed/Lbry.js b/dist/flow-typed/Lbry.js index a403ffb..68ccc93 100644 --- a/dist/flow-typed/Lbry.js +++ b/dist/flow-typed/Lbry.js @@ -65,7 +65,7 @@ declare type VersionResponse = { declare type ResolveResponse = { // Keys are the url(s) passed to resolve - [string]: Claim | { error?: {} }, + [string]: { error?: {}, stream?: StreamClaim, channel?: ChannelClaim, claimsInChannel?: number }, }; declare type GetResponse = FileListItem & { error?: string }; diff --git a/dist/flow-typed/i18n.js b/dist/flow-typed/i18n.js new file mode 100644 index 0000000..050d684 --- /dev/null +++ b/dist/flow-typed/i18n.js @@ -0,0 +1,2 @@ +// @flow +declare function __(a: string, b?: string | number): string; diff --git a/flow-typed/Claim.js b/flow-typed/Claim.js index 1c61c10..54e414f 100644 --- a/flow-typed/Claim.js +++ b/flow-typed/Claim.js @@ -23,6 +23,7 @@ declare type GenericClaim = { decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 timestamp?: number, // date of last transaction height: number, // block height the tx was confirmed + is_mine: boolean, name: string, normalized_name: string, // `name` normalized via unicode NFD spec, nout: number, // index number for an output of a tx diff --git a/flow-typed/Lbry.js b/flow-typed/Lbry.js index a403ffb..68ccc93 100644 --- a/flow-typed/Lbry.js +++ b/flow-typed/Lbry.js @@ -65,7 +65,7 @@ declare type VersionResponse = { declare type ResolveResponse = { // Keys are the url(s) passed to resolve - [string]: Claim | { error?: {} }, + [string]: { error?: {}, stream?: StreamClaim, channel?: ChannelClaim, claimsInChannel?: number }, }; declare type GetResponse = FileListItem & { error?: string }; diff --git a/flow-typed/i18n.js b/flow-typed/i18n.js new file mode 100644 index 0000000..050d684 --- /dev/null +++ b/flow-typed/i18n.js @@ -0,0 +1,2 @@ +// @flow +declare function __(a: string, b?: string | number): string; diff --git a/src/index.js b/src/index.js index 88bdf74..1370c3c 100644 --- a/src/index.js +++ b/src/index.js @@ -172,6 +172,7 @@ export { makeSelectPendingByUri, makeSelectClaimsInChannelForCurrentPageState, makeSelectShortUrlForUri, + makeSelectCanonicalUrlForUri, makeSelectSupportsForUri, selectPendingById, selectClaimsById, diff --git a/src/lbryURI.js b/src/lbryURI.js index 72a6200..323d509 100644 --- a/src/lbryURI.js +++ b/src/lbryURI.js @@ -1,46 +1,77 @@ +// @flow +const isProduction = process.env.NODE_ENV === 'production'; const channelNameMinLength = 1; const claimIdMaxLength = 40; // see https://spec.lbry.com/#urls export const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u; export const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/; +const regexPartProtocol = '^((?:lbry://)?)'; +const regexPartStreamOrChannelName = '([^:$#/]*)'; +const regexPartModifierSeparator = '([:$#]?)([^/]*)'; /** * Parses a LBRY name into its component parts. Throws errors with user-friendly * messages for invalid names. * - * N.B. that "name" indicates the value in the name position of the URI. For - * claims for channel content, this will actually be the channel name, and - * the content name is in the path (e.g. lbry://@channel/content) - * - * In most situations, you'll want to use the contentName and channelName keys - * and ignore the name key. - * * Returns a dictionary with keys: - * - name (string): The value in the "name" position in the URI. Note that this - * could be either content name or channel name; see above. - * - path (string, if persent) - * - claimSequence (int, if present) - * - bidPosition (int, if present) - * - claimId (string, if present) + * - path (string) * - isChannel (boolean) - * - contentName (string): For anon claims, the name; for channel claims, the path - * - channelName (string, if present): Channel name without @ + * - streamName (string, if present) + * - streamClaimId (string, if present) + * - channelName (string, if present) + * - channelClaimId (string, if present) + * - primaryClaimSequence (int, if present) + * - secondaryClaimSequence (int, if present) + * - primaryBidPosition (int, if present) + * - secondaryBidPosition (int, if present) */ -export function parseURI(URI, requireProto = false) { + +type ChannelUrlObj = {}; + +type LbryUrlObj = { + // Path and channel will always exist when calling parseURI + // But they may not exist when code calls buildURI + isChannel?: boolean, + path?: string, + streamName?: string, + streamClaimId?: string, + channelName?: string, + channelClaimId?: string, + primaryClaimSequence?: number, + secondaryClaimSequence?: number, + primaryBidPosition?: number, + secondaryBidPosition?: number, + + // Below are considered deprecated and should not be used due to unreliableness with claim.canonical_url + claimName?: string, + claimId?: string, + contentName?: string, +}; + +export function parseURI(URL: string, requireProto: boolean = false): LbryUrlObj { // Break into components. Empty sub-matches are converted to null const componentsRegex = new RegExp( - '^((?:lbry://)?)' + // protocol - '([^:$#/]*)' + // claim name (stops at the first separator or end) - '([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) - '(/?)(.*)' // path separator, path + regexPartProtocol + // protocol + regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end) + regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end) + '(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts + regexPartStreamOrChannelName + + regexPartModifierSeparator ); - const [proto, claimName, modSep, modVal, pathSep, path] = componentsRegex - .exec(URI) - .slice(1) - .map(match => match || null); - let contentName; + const regexMatch = componentsRegex.exec(URL) || []; + const [proto, ...rest] = regexMatch.slice(1).map(match => match || null); + const path = rest.join(''); + const [ + streamNameOrChannelName, + primaryModSeparator, + primaryModValue, + pathSep, + possibleStreamName, + secondaryModSeparator, + secondaryModValue, + ] = rest; // Validate protocol if (requireProto && !proto) { @@ -48,14 +79,15 @@ export function parseURI(URI, requireProto = false) { } // Validate and process name - if (!claimName) { + if (!streamNameOrChannelName) { throw new Error(__('URI does not include name.')); } - const isChannel = claimName.startsWith('@'); - const channelName = isChannel ? claimName.slice(1) : claimName; + const includesChannel = streamNameOrChannelName.startsWith('@'); + const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName; + const channelName = includesChannel && streamNameOrChannelName.slice(1); - if (isChannel) { + if (includesChannel) { if (!channelName) { throw new Error(__('No channel name after @.')); } @@ -63,36 +95,58 @@ export function parseURI(URI, requireProto = false) { if (channelName.length < channelNameMinLength) { throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength)); } - - contentName = path; } - const nameBadChars = (channelName || claimName).match(regexInvalidURI); - if (nameBadChars) { - throw new Error( - __( - `Invalid character %s in name: %s.`, - nameBadChars.length === 1 ? '' : 's', - nameBadChars.join(', ') - ) - ); - } + // Validate and process modifier + const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier( + primaryModSeparator, + primaryModValue + ); + const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier( + secondaryModSeparator, + secondaryModValue + ); + const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName; + const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId; + const channelClaimId = includesChannel && primaryClaimId; - // Validate and process modifier (claim ID, bid position or claim sequence) + return { + isChannel, + path, + ...(streamName ? { streamName } : {}), + ...(streamClaimId ? { streamClaimId } : {}), + ...(channelName ? { channelName } : {}), + ...(channelClaimId ? { channelClaimId } : {}), + ...(primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}), + ...(secondaryClaimSequence + ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } + : {}), + ...(primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}), + ...(secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}), + + // The values below should not be used for new uses of parseURI + // They will not work properly with canonical_urls + claimName: streamNameOrChannelName, + claimId: primaryClaimId, + ...(streamName ? { contentName: streamName } : {}), + }; +} + +function parseURIModifier(modSeperator: ?string, modValue: ?string) { let claimId; let claimSequence; let bidPosition; - if (modSep) { - if (!modVal) { - throw new Error(__(`No modifier provided after separator %s.`, modSep)); + if (modSeperator) { + if (!modValue) { + throw new Error(__(`No modifier provided after separator %s.`, modSeperator)); } - if (modSep === '#') { - claimId = modVal; - } else if (modSep === ':') { - claimSequence = modVal; - } else if (modSep === '$') { - bidPosition = modVal; + if (modSeperator === '#') { + claimId = modValue; + } else if (modSeperator === ':') { + claimSequence = modValue; + } else if (modSeperator === '$') { + bidPosition = modValue; } } @@ -108,33 +162,7 @@ export function parseURI(URI, requireProto = false) { throw new Error(__('Bid position must be a number.')); } - // Validate and process path - if (path) { - if (!isChannel) { - throw new Error(__('Only channel URIs may have a path.')); - } - - const pathBadChars = path.match(regexInvalidURI); - if (pathBadChars) { - throw new Error(__(`Invalid character in path: %s`, pathBadChars.join(', '))); - } - - contentName = path; - } else if (pathSep) { - throw new Error(__('No path provided after /')); - } - - return { - claimName, - path, - isChannel, - ...(contentName ? { contentName } : {}), - ...(channelName ? { channelName } : {}), - ...(claimSequence ? { claimSequence: parseInt(claimSequence, 10) } : {}), - ...(bidPosition ? { bidPosition: parseInt(bidPosition, 10) } : {}), - ...(claimId ? { claimId } : {}), - ...(path ? { path } : {}), - }; + return [claimId, claimSequence, bidPosition]; } /** @@ -142,91 +170,144 @@ export function parseURI(URI, requireProto = false) { * * The channelName key will accept names with or without the @ prefix. */ -export function buildURI(URIObj, includeProto = true, protoDefault = 'lbry://') { - const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj; +export function buildURI( + UrlObj: LbryUrlObj, + includeProto: boolean = true, + protoDefault: string = 'lbry://' +): string { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + ...deprecatedParts + } = UrlObj; + const { claimId, claimName, contentName } = deprecatedParts; - let { claimName, path } = URIObj; - - if (channelName) { - const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`; - if (!claimName) { - claimName = channelNameFormatted; - } else if (claimName !== channelNameFormatted) { - throw new Error( + if (!isProduction) { + if (claimId) { + console.error( + __("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead") + ); + } + if (claimName) { + console.error( __( - 'Received a channel content URI, but claim name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.' + "'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead" ) ); } + if (contentName) { + console.error(__("'contentName' should no longer be used. Use 'streamName' instead")); + } } - if (contentName) { - if (!claimName) { - claimName = contentName; - } else if (!path) { - path = contentName; - } - if (path && path !== contentName) { - throw new Error( - __( - 'Path and contentName do not match. Only one is required; most likely you wanted contentName.' - ) - ); - } + if (!claimName && !channelName && !streamName) { + throw new Error( + __( + "'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url." + ) + ); } + const formattedChannelName = + channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`); + const primaryClaimName = claimName || formattedChannelName || streamName; + const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId); + const secondaryClaimName = !claimName && (formattedChannelName ? streamName : null); + const secondaryClaimId = secondaryClaimName && streamClaimId; + return ( (includeProto ? protoDefault : '') + - claimName + - (claimId ? `#${claimId}` : '') + - (claimSequence ? `:${claimSequence}` : '') + - (bidPosition ? `${bidPosition}` : '') + - (path ? `/${path}` : '') + // primaryClaimName will always exist here because we throw above if there is no "name" value passed in + // $FlowFixMe + primaryClaimName + + (primaryClaimId ? `#${primaryClaimId}` : '') + + (primaryClaimSequence ? `:${primaryClaimSequence}` : '') + + (primaryBidPosition ? `${primaryBidPosition}` : '') + + (secondaryClaimName ? `/${secondaryClaimName}` : '') + + (secondaryClaimId ? `#${secondaryClaimId}` : '') + + (secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') + + (secondaryBidPosition ? `${secondaryBidPosition}` : '') ); } -/* Takes a parseable LBRY URI and converts it to standard, canonical format */ -export function normalizeURI(URI) { - const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); - return buildURI({ claimName, path, claimSequence, bidPosition, claimId }); +/* Takes a parseable LBRY URL and converts it to standard, canonical format */ +export function normalizeURI(URL: string) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + } = parseURI(URL); + + return buildURI({ + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + }); } -export function isURIValid(URI) { - let parts; +export function isURIValid(URL: string): boolean { try { - parts = parseURI(normalizeURI(URI)); + parseURI(normalizeURI(URL)); } catch (error) { return false; } - return parts && parts.claimName; + + return true; } -export function isNameValid(claimName) { +export function isNameValid(claimName: string) { return !regexInvalidURI.test(claimName); } -export function isURIClaimable(URI) { +export function isURIClaimable(URL: string) { let parts; try { - parts = parseURI(normalizeURI(URI)); + parts = parseURI(normalizeURI(URL)); } catch (error) { return false; } - return ( - parts && - parts.claimName && - !parts.claimId && - !parts.bidPosition && - !parts.claimSequence && - !parts.isChannel && - !parts.path - ); + + return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel; } -export function convertToShareLink(URI) { - const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); +export function convertToShareLink(URL: string) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence, + } = parseURI(URL); return buildURI( - { claimName, path, claimSequence, bidPosition, claimId }, + { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence, + }, true, 'https://open.lbry.com/' ); diff --git a/src/redux/actions/claims.js b/src/redux/actions/claims.js index 0f6e44e..ab03679 100644 --- a/src/redux/actions/claims.js +++ b/src/redux/actions/claims.js @@ -328,10 +328,10 @@ export function doClaimSearch( const success = (data: ClaimSearchResponse) => { const resolveInfo = {}; - const uris = []; + const urls = []; data.items.forEach((stream: Claim) => { - resolveInfo[stream.permanent_url] = { stream }; - uris.push(stream.permanent_url); + resolveInfo[stream.canonical_url] = { stream }; + urls.push(stream.canonical_url); }); dispatch({ @@ -339,7 +339,7 @@ export function doClaimSearch( data: { query, resolveInfo, - uris, + urls, append: options.page && options.page !== 1, pageSize: options.page_size, }, diff --git a/src/redux/actions/search.js b/src/redux/actions/search.js index 8108e9c..2e134bc 100644 --- a/src/redux/actions/search.js +++ b/src/redux/actions/search.js @@ -112,18 +112,26 @@ export const doSearch = ( fetch(`${CONNECTION_STRING}search?${queryWithOptions}`) .then(handleFetchResponse) - .then((data: Array<{ name: String, claimId: string }>) => { + .then((data: Array<{ name: string, claimId: string }>) => { const uris = []; const actions = []; data.forEach(result => { - if (result.name) { - const uri = buildURI({ - claimName: result.name, - claimId: result.claimId, - }); - actions.push(doResolveUri(uri)); - uris.push(uri); + if (result) { + const { name, claimId } = result; + const urlObj = {}; + + if (name.startsWith('@')) { + urlObj.channelName = name; + urlObj.channelClaimId = claimId; + } else { + urlObj.streamName = name; + urlObj.streamClaimId = claimId; + } + + const url = buildURI(urlObj); + actions.push(doResolveUri(url)); + uris.push(url); } }); diff --git a/src/redux/reducers/claims.js b/src/redux/reducers/claims.js index ba94348..4aacad6 100644 --- a/src/redux/reducers/claims.js +++ b/src/redux/reducers/claims.js @@ -66,26 +66,25 @@ function handleClaimAction(state: State, action: any): State { const byId = Object.assign({}, state.byId); const channelClaimCounts = Object.assign({}, state.channelClaimCounts); - Object.entries(resolveInfo).forEach(([uri: string, resolveResponse: Claim]) => { + Object.entries(resolveInfo).forEach(([url: string, resolveResponse: ResolveResponse]) => { // $FlowFixMe - if (resolveResponse.claimsInChannel) { - // $FlowFixMe - channelClaimCounts[uri] = resolveResponse.claimsInChannel; + const { claimsInChannel, stream, channel } = resolveResponse; + if (claimsInChannel) { + channelClaimCounts[url] = claimsInChannel; } - }); - // $FlowFixMe - Object.entries(resolveInfo).forEach(([uri, { channel, stream }]) => { if (stream) { byId[stream.claim_id] = stream; - byUri[uri] = stream.claim_id; + byUri[url] = stream.claim_id; } + if (channel) { byId[channel.claim_id] = channel; - byUri[stream ? channel.permanent_url : uri] = channel.claim_id; + byUri[stream ? channel.canonical_url : url] = channel.claim_id; } + if (!stream && !channel) { - byUri[uri] = null; + byUri[url] = null; } }); @@ -305,20 +304,20 @@ reducers[ACTIONS.CLAIM_SEARCH_COMPLETED] = (state: State, action: any): State => {}, state.claimSearchByQueryLastPageReached ); - const { append, query, uris, pageSize } = action.data; + const { append, query, urls, pageSize } = action.data; if (append) { - // todo: check for duplicate uris when concatenating? + // todo: check for duplicate urls when concatenating? claimSearchByQuery[query] = claimSearchByQuery[query] && claimSearchByQuery[query].length - ? claimSearchByQuery[query].concat(uris) - : uris; + ? claimSearchByQuery[query].concat(urls) + : urls; } else { - claimSearchByQuery[query] = uris; + claimSearchByQuery[query] = urls; } - // the returned number of uris is less than the page size, so we're on the last page - claimSearchByQueryLastPageReached[query] = uris.length < pageSize; + // the returned number of urls is less than the page size, so we're on the last page + claimSearchByQueryLastPageReached[query] = urls.length < pageSize; delete fetchingClaimSearchByQuery[query]; diff --git a/src/redux/selectors/claims.js b/src/redux/selectors/claims.js index 5a38364..26cbc7b 100644 --- a/src/redux/selectors/claims.js +++ b/src/redux/selectors/claims.js @@ -62,8 +62,10 @@ export const makeSelectClaimIsPending = (uri: string) => selectPendingById, pendingById => { let claimId; + try { - ({ claimId } = parseURI(uri)); + const { isChannel, channelClaimId, streamClaimId } = parseURI(uri); + claimId = isChannel ? channelClaimId : streamClaimId; } catch (e) {} if (claimId) { @@ -76,7 +78,8 @@ export const makeSelectPendingByUri = (uri: string) => createSelector( selectPendingById, pendingById => { - const { claimId } = parseURI(uri); + const { isChannel, channelClaimId, streamClaimId } = parseURI(uri); + const claimId = isChannel ? channelClaimId : streamClaimId; return pendingById[claimId]; } ); @@ -90,13 +93,16 @@ export const makeSelectClaimForUri = (uri: string) => // It won't be in claimsByUri because resolving it will return nothing let valid; - let claimId; + let channelClaimId; + let streamClaimId; + let isChannel; try { - ({ claimId } = parseURI(uri)); + ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri)); valid = true; } catch (e) {} if (valid) { + const claimId = isChannel ? channelClaimId : streamClaimId; const pendingClaim = pendingById[claimId]; if (pendingClaim) { @@ -521,23 +527,18 @@ export const selectClaimSearchByQueryLastPageReached = createSelector( state => state.claimSearchByQueryLastPageReached || {} ); -export const makeSelectClaimSearchUrisByOptions = (options: {}) => - createSelector( - selectClaimSearchByQuery, - byQuery => { - // We don't care what options are passed to this selector. Just forward them. - // $FlowFixMe - const query = createNormalizedClaimSearchKey(options); - return byQuery[query]; - } - ); - export const makeSelectShortUrlForUri = (uri: string) => createSelector( makeSelectClaimForUri(uri), claim => claim && claim.short_url ); +export const makeSelectCanonicalUrlForUri = (uri: string) => + createSelector( + makeSelectClaimForUri(uri), + claim => claim && claim.canonical_url + ); + export const makeSelectSupportsForUri = (uri: string) => createSelector( selectSupportsByOutpoint, @@ -548,11 +549,12 @@ export const makeSelectSupportsForUri = (uri: string) => } const { claim_id: claimId } = claim; - let total = parseFloat("0.0"); + let total = 0; Object.values(byOutpoint).forEach(support => { - const { claim_id, amount } = support - total = (claim_id === claimId && amount) ? total + parseFloat(amount) : total; + // $FlowFixMe + const { claim_id, amount } = support; + total = claim_id === claimId && amount ? total + parseFloat(amount) : total; }); return total; diff --git a/src/redux/selectors/publish.js b/src/redux/selectors/publish.js index 37f0dab..bfe4390 100644 --- a/src/redux/selectors/publish.js +++ b/src/redux/selectors/publish.js @@ -33,8 +33,16 @@ export const selectIsStillEditing = createSelector( return false; } - const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); - const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); + 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 @@ -60,7 +68,9 @@ export const selectMyClaimForUri = createSelector( return isStillEditing ? claimsById[editClaimId] : myClaims.find(claim => - !contentName ? claim.name === claimName : claim.name === contentName || claim.name === claimName + !contentName + ? claim.name === claimName + : claim.name === contentName || claim.name === claimName ); } ); @@ -75,7 +85,7 @@ export const selectIsResolvingPublishUris = createSelector( let isResolvingShortUri; if (isChannel) { - const shortUri = buildURI({ contentName: name }); + const shortUri = buildURI({ streamName: name }); isResolvingShortUri = resolvingUris.includes(shortUri); } diff --git a/src/redux/selectors/search.js b/src/redux/selectors/search.js index 660228f..e533af1 100644 --- a/src/redux/selectors/search.js +++ b/src/redux/selectors/search.js @@ -77,15 +77,15 @@ export const selectSearchSuggestions: Array = createSelector( let searchSuggestions = []; try { const uri = normalizeURI(query); - const { claimName, isChannel } = parseURI(uri); + const { channelName, streamName, isChannel } = parseURI(uri); searchSuggestions.push( { - value: claimName, + value: streamName, type: SEARCH_TYPES.SEARCH, }, { value: uri, - shorthand: isChannel ? claimName.slice(1) : claimName, + shorthand: isChannel ? channelName : streamName, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, } ); @@ -110,11 +110,11 @@ export const selectSearchSuggestions: Array = createSelector( // determine if it's a channel try { const uri = normalizeURI(suggestion); - const { claimName, isChannel } = parseURI(uri); + const { channelName, streamName, isChannel } = parseURI(uri); return { value: uri, - shorthand: isChannel ? claimName.slice(1) : claimName, + shorthand: isChannel ? channelName : streamName, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, }; } catch (e) { diff --git a/src/util/uri.js b/src/util/uri.js deleted file mode 100644 index 238126a..0000000 --- a/src/util/uri.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import { parseURI } from 'lbryURI'; - -export const formatLbryUriForWeb = (uri: string) => { - const { claimName, claimId } = parseURI(uri); - - let webUrl = `/${claimName}`; - if (claimId) { - webUrl += `/${claimId}`; - } - - return webUrl; -}; From 22879b2880d96fd8993ea0ca48f2582e32e9897c Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Thu, 22 Aug 2019 11:03:13 -0400 Subject: [PATCH 2/2] updates --- dist/bundle.es.js | 19 ++----------------- flow-typed/lbryURI.js | 20 ++++++++++++++++++++ package.json | 5 +++-- rollup.config.js | 8 ++++++++ src/lbryURI.js | 27 +++------------------------ src/redux/actions/search.js | 2 +- yarn.lock | 32 ++++++++++++++++++++++++++++++++ 7 files changed, 69 insertions(+), 44 deletions(-) create mode 100644 flow-typed/lbryURI.js diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 45fb468..53a51a9 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -898,9 +898,6 @@ const getSearchQueryString = (query, options = {}, includeUserOptions = false) = var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -// -const isProduction = process.env.NODE_ENV === 'production'; const channelNameMinLength = 1; const claimIdMaxLength = 40; @@ -1036,26 +1033,14 @@ function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') { deprecatedParts = _objectWithoutProperties(UrlObj, ['streamName', 'streamClaimId', 'channelName', 'channelClaimId', 'primaryClaimSequence', 'primaryBidPosition', 'secondaryClaimSequence', 'secondaryBidPosition']); const { claimId, claimName, contentName } = deprecatedParts; - if (!isProduction) { - if (claimId) { - console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")); - } - if (claimName) { - console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead")); - } - if (contentName) { - console.error(__("'contentName' should no longer be used. Use 'streamName' instead")); - } - } - if (!claimName && !channelName && !streamName) { throw new Error(__("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.")); } const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`); - const primaryClaimName = claimName || formattedChannelName || streamName; + const primaryClaimName = claimName || contentName || formattedChannelName || streamName; const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId); - const secondaryClaimName = !claimName && (formattedChannelName ? streamName : null); + const secondaryClaimName = !claimName && contentName || (formattedChannelName ? streamName : null); const secondaryClaimId = secondaryClaimName && streamClaimId; return (includeProto ? protoDefault : '') + diff --git a/flow-typed/lbryURI.js b/flow-typed/lbryURI.js new file mode 100644 index 0000000..4365da3 --- /dev/null +++ b/flow-typed/lbryURI.js @@ -0,0 +1,20 @@ +// @flow +declare type LbryUrlObj = { + // Path and channel will always exist when calling parseURI + // But they may not exist when code calls buildURI + isChannel?: boolean, + path?: string, + streamName?: string, + streamClaimId?: string, + channelName?: string, + channelClaimId?: string, + primaryClaimSequence?: number, + secondaryClaimSequence?: number, + primaryBidPosition?: number, + secondaryBidPosition?: number, + + // Below are considered deprecated and should not be used due to unreliableness with claim.canonical_url + claimName?: string, + claimId?: string, + contentName?: string, +}; diff --git a/package.json b/package.json index 21b595f..ca6b815 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "main": "dist/bundle.es.js", "module": "dist/bundle.es.js", "scripts": { - "build": "rollup --config", + "build": "NODE_ENV=production rollup --config", "dev": "rollup --config --watch", "precommit": "flow check && lint-staged", "lint": "eslint 'src/**/*.js' --fix", @@ -59,7 +59,8 @@ "rollup-plugin-copy": "^1.1.0", "rollup-plugin-eslint": "^5.1.0", "rollup-plugin-flow": "^1.1.1", - "rollup-plugin-includepaths": "^0.2.3" + "rollup-plugin-includepaths": "^0.2.3", + "rollup-plugin-replace": "^2.2.0" }, "engines": { "yarn": "^1.3" diff --git a/rollup.config.js b/rollup.config.js index 30de832..2b898d6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,6 +2,7 @@ import babel from 'rollup-plugin-babel'; import flow from 'rollup-plugin-flow'; import includePaths from 'rollup-plugin-includepaths'; import copy from 'rollup-plugin-copy'; +import replace from 'rollup-plugin-replace'; let includePathOptions = { include: {}, @@ -10,6 +11,8 @@ let includePathOptions = { extensions: ['.js'], }; +const production = process.env.NODE_ENV === 'production'; + export default { input: 'src/index.js', output: { @@ -24,5 +27,10 @@ export default { presets: ['stage-2'], }), copy({ targets: ['flow-typed'] }), + replace({ + 'process.env.NODE_ENV': production + ? JSON.stringify('production') + : JSON.stringify('development'), + }), ], }; diff --git a/src/lbryURI.js b/src/lbryURI.js index 323d509..3a67e6e 100644 --- a/src/lbryURI.js +++ b/src/lbryURI.js @@ -27,28 +27,6 @@ const regexPartModifierSeparator = '([:$#]?)([^/]*)'; * - secondaryBidPosition (int, if present) */ -type ChannelUrlObj = {}; - -type LbryUrlObj = { - // Path and channel will always exist when calling parseURI - // But they may not exist when code calls buildURI - isChannel?: boolean, - path?: string, - streamName?: string, - streamClaimId?: string, - channelName?: string, - channelClaimId?: string, - primaryClaimSequence?: number, - secondaryClaimSequence?: number, - primaryBidPosition?: number, - secondaryBidPosition?: number, - - // Below are considered deprecated and should not be used due to unreliableness with claim.canonical_url - claimName?: string, - claimId?: string, - contentName?: string, -}; - export function parseURI(URL: string, requireProto: boolean = false): LbryUrlObj { // Break into components. Empty sub-matches are converted to null const componentsRegex = new RegExp( @@ -216,9 +194,10 @@ export function buildURI( const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`); - const primaryClaimName = claimName || formattedChannelName || streamName; + const primaryClaimName = claimName || contentName || formattedChannelName || streamName; const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId); - const secondaryClaimName = !claimName && (formattedChannelName ? streamName : null); + const secondaryClaimName = + (!claimName && contentName) || (formattedChannelName ? streamName : null); const secondaryClaimId = secondaryClaimName && streamClaimId; return ( diff --git a/src/redux/actions/search.js b/src/redux/actions/search.js index 2e134bc..d5c6ce3 100644 --- a/src/redux/actions/search.js +++ b/src/redux/actions/search.js @@ -119,7 +119,7 @@ export const doSearch = ( data.forEach(result => { if (result) { const { name, claimId } = result; - const urlObj = {}; + const urlObj: LbryUrlObj = {}; if (name.startsWith('@')) { urlObj.channelName = name; diff --git a/yarn.lock b/yarn.lock index 49dc513..156e422 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1911,6 +1911,11 @@ estree-walker@^0.6.0: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae" integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw== +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -3440,6 +3445,13 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +magic-string@^0.25.2: + version "0.25.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.3.tgz#34b8d2a2c7fec9d9bdf9929a3fd81d271ef35be9" + integrity sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" @@ -4379,6 +4391,14 @@ rollup-plugin-includepaths@^0.2.3: resolved "https://registry.yarnpkg.com/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.3.tgz#244d21b9669a0debe476d825e4a02ed08c06b258" integrity sha512-4QbSIZPDT+FL4SViEVCRi4cGCA64zQJu7u5qmCkO3ecHy+l9EQBsue15KfCpddfb6Br0q47V/v2+E2YUiqts9g== +rollup-plugin-replace@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3" + integrity sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA== + dependencies: + magic-string "^0.25.2" + rollup-pluginutils "^2.6.0" + rollup-pluginutils@^1.5.0, rollup-pluginutils@^1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" @@ -4395,6 +4415,13 @@ rollup-pluginutils@^2.3.0: estree-walker "^0.6.0" micromatch "^3.1.10" +rollup-pluginutils@^2.6.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97" + integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg== + dependencies: + estree-walker "^0.6.1" + rollup@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.8.0.tgz#e3ce8b708ad4325166717f74f244f691595d35e2" @@ -4613,6 +4640,11 @@ source-map@^0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sourcemap-codec@^1.4.4: + version "1.4.6" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" + integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== + spdx-correct@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"