use canonical_url everywhere and update url parse/build functions to work with canonical_url's properly

This commit is contained in:
Sean Yesmunt 2019-08-20 16:00:26 -04:00
parent 4f812db1c7
commit f5289f9811
17 changed files with 515 additions and 346 deletions

View file

@ -6,6 +6,7 @@
./flow-typed ./flow-typed
[options] [options]
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
module.system.node.resolve_dirname=./src module.system.node.resolve_dirname=./src
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/redux\1' module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/redux\1'
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/util\1' module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/util\1'

362
dist/bundle.es.js vendored
View file

@ -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; }; 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 channelNameMinLength = 1;
const claimIdMaxLength = 40; const claimIdMaxLength = 40;
// see https://spec.lbry.com/#urls // 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 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 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 * Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names. * 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: * Returns a dictionary with keys:
* - name (string): The value in the "name" position in the URI. Note that this * - path (string)
* 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)
* - isChannel (boolean) * - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path * - streamName (string, if present)
* - channelName (string, if present): Channel name without @ * - 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 // Validate protocol
if (requireProto && !proto) { if (requireProto && !proto) {
@ -943,14 +947,15 @@ function parseURI(URI, requireProto = false) {
} }
// Validate and process name // Validate and process name
if (!claimName) { if (!streamNameOrChannelName) {
throw new Error(__('URI does not include name.')); throw new Error(__('URI does not include name.'));
} }
const isChannel = claimName.startsWith('@'); const includesChannel = streamNameOrChannelName.startsWith('@');
const channelName = isChannel ? claimName.slice(1) : claimName; const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
const channelName = includesChannel && streamNameOrChannelName.slice(1);
if (isChannel) { if (includesChannel) {
if (!channelName) { if (!channelName) {
throw new Error(__('No channel name after @.')); throw new Error(__('No channel name after @.'));
} }
@ -958,30 +963,42 @@ function parseURI(URI, requireProto = false) {
if (channelName.length < channelNameMinLength) { if (channelName.length < channelNameMinLength) {
throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength)); throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength));
} }
contentName = path;
} }
const nameBadChars = (channelName || claimName).match(regexInvalidURI); // Validate and process modifier
if (nameBadChars) { const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(primaryModSeparator, primaryModValue);
throw new Error(__(`Invalid character %s in name: %s.`, nameBadChars.length === 1 ? '' : 's', nameBadChars.join(', '))); 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 claimId;
let claimSequence; let claimSequence;
let bidPosition; let bidPosition;
if (modSep) { if (modSeperator) {
if (!modVal) { if (!modValue) {
throw new Error(__(`No modifier provided after separator %s.`, modSep)); throw new Error(__(`No modifier provided after separator %s.`, modSeperator));
} }
if (modSep === '#') { if (modSeperator === '#') {
claimId = modVal; claimId = modValue;
} else if (modSep === ':') { } else if (modSeperator === ':') {
claimSequence = modVal; claimSequence = modValue;
} else if (modSep === '$') { } else if (modSeperator === '$') {
bidPosition = modVal; bidPosition = modValue;
} }
} }
@ -997,27 +1014,7 @@ function parseURI(URI, requireProto = false) {
throw new Error(__('Bid position must be a number.')); throw new Error(__('Bid position must be a number.'));
} }
// Validate and process path return [claimId, claimSequence, bidPosition];
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 } : {});
} }
/** /**
@ -1025,67 +1022,119 @@ function parseURI(URI, requireProto = false) {
* *
* The channelName key will accept names with or without the @ prefix. * The channelName key will accept names with or without the @ prefix.
*/ */
function buildURI(URIObj, includeProto = true, protoDefault = 'lbry://') { function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') {
const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj; 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 (!isProduction) {
if (claimId) {
if (channelName) { console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead"));
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 (claimName) {
console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead"));
} }
if (contentName) { if (contentName) {
if (!claimName) { console.error(__("'contentName' should no longer be used. Use 'streamName' instead"));
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.'));
} }
} }
return (includeProto ? protoDefault : '') + claimName + (claimId ? `#${claimId}` : '') + (claimSequence ? `:${claimSequence}` : '') + (bidPosition ? `${bidPosition}` : '') + (path ? `/${path}` : ''); 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 : '') +
// 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 */ /* Takes a parseable LBRY URL and converts it to standard, canonical format */
function normalizeURI(URI) { function normalizeURI(URL) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
return buildURI({ claimName, path, claimSequence, bidPosition, claimId }); streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition
} = parseURI(URL);
return buildURI({
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition
});
} }
function isURIValid(URI) { function isURIValid(URL) {
let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.claimName;
return true;
} }
function isNameValid(claimName) { function isNameValid(claimName) {
return !regexInvalidURI.test(claimName); return !regexInvalidURI.test(claimName);
} }
function isURIClaimable(URI) { function isURIClaimable(URL) {
let parts; let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parts = parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; 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) { function convertToShareLink(URL) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
return buildURI({ claimName, path, claimSequence, bidPosition, claimId }, true, 'https://open.lbry.com/'); 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; }; 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 = []; let searchSuggestions = [];
try { try {
const uri = normalizeURI(query); const uri = normalizeURI(query);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
searchSuggestions.push({ searchSuggestions.push({
value: claimName, value: streamName,
type: SEARCH_TYPES.SEARCH type: SEARCH_TYPES.SEARCH
}, { }, {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE
}); });
} catch (e) { } catch (e) {
@ -1157,11 +1206,11 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec
// determine if it's a channel // determine if it's a channel
try { try {
const uri = normalizeURI(suggestion); const uri = normalizeURI(suggestion);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
return { return {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE
}; };
} catch (e) { } 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; }; 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 }), {}); const matureTagMap = MATURE_TAGS.reduce((acc, tag) => _extends$2({}, acc, { [tag]: true }), {});
@ -1404,7 +1453,7 @@ const isClaimNsfw = claim => {
function createNormalizedClaimSearchKey(options) { function createNormalizedClaimSearchKey(options) {
// Ignore page because we don't care what the last page searched was, we want everything // 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" // 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); const query = JSON.stringify(rest);
return query; return query;
} }
@ -1445,8 +1494,10 @@ const selectPendingClaims = reselect.createSelector(selectState$2, state => Obje
const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingById, pendingById => { const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingById, pendingById => {
let claimId; let claimId;
try { try {
({ claimId } = parseURI(uri)); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
claimId = isChannel ? channelClaimId : streamClaimId;
} catch (e) {} } catch (e) {}
if (claimId) { if (claimId) {
@ -1455,7 +1506,8 @@ const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingByI
}); });
const makeSelectPendingByUri = uri => reselect.createSelector(selectPendingById, pendingById => { 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]; 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 // It won't be in claimsByUri because resolving it will return nothing
let valid; let valid;
let claimId; let channelClaimId;
let streamClaimId;
let isChannel;
try { try {
({ claimId } = parseURI(uri)); ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri));
valid = true; valid = true;
} catch (e) {} } catch (e) {}
if (valid) { if (valid) {
const claimId = isChannel ? channelClaimId : streamClaimId;
const pendingClaim = pendingById[claimId]; const pendingClaim = pendingById[claimId];
if (pendingClaim) { if (pendingClaim) {
@ -1720,15 +1775,18 @@ const selectClaimSearchByQueryLastPageReached = reselect.createSelector(selectSt
const makeSelectShortUrlForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), claim => claim && claim.short_url); 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) => { const makeSelectSupportsForUri = uri => reselect.createSelector(selectSupportsByOutpoint, makeSelectClaimForUri(uri), (byOutpoint, claim) => {
if (!claim || !claim.is_mine) { if (!claim || !claim.is_mine) {
return null; return null;
} }
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
let total = parseFloat("0.0"); let total = 0;
Object.values(byOutpoint).forEach(support => { Object.values(byOutpoint).forEach(support => {
// $FlowFixMe
const { claim_id, amount } = support; const { claim_id, amount } = support;
total = claim_id === claimId && amount ? total + parseFloat(amount) : total; total = claim_id === claimId && amount ? total + parseFloat(amount) : total;
}); });
@ -2433,10 +2491,10 @@ function doClaimSearch(options = {
const success = data => { const success = data => {
const resolveInfo = {}; const resolveInfo = {};
const uris = []; const urls = [];
data.items.forEach(stream => { data.items.forEach(stream => {
resolveInfo[stream.permanent_url] = { stream }; resolveInfo[stream.canonical_url] = { stream };
uris.push(stream.permanent_url); urls.push(stream.canonical_url);
}); });
dispatch({ dispatch({
@ -2444,7 +2502,7 @@ function doClaimSearch(options = {
data: { data: {
query, query,
resolveInfo, resolveInfo,
uris, urls,
append: options.page && options.page !== 1, append: options.page && options.page !== 1,
pageSize: options.page_size 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 selectState$5 = state => state.publish || {};
const selectPublishFormValues = reselect.createSelector(selectState$5, state => { const selectPublishFormValues = reselect.createSelector(selectState$5, state => {
const formValues = _objectWithoutProperties$1(state, ['pendingPublish']); const formValues = _objectWithoutProperties$2(state, ['pendingPublish']);
return formValues; return formValues;
}); });
const makeSelectPublishFormValue = item => reselect.createSelector(selectState$5, state => state[item]); const makeSelectPublishFormValue = item => reselect.createSelector(selectState$5, state => state[item]);
@ -2876,8 +2934,16 @@ const selectIsStillEditing = reselect.createSelector(selectPublishFormValues, pu
return false; return false;
} }
const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); const {
const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); 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 // 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 // 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; let isResolvingShortUri;
if (isChannel) { if (isChannel) {
const shortUri = buildURI({ contentName: name }); const shortUri = buildURI({ streamName: name });
isResolvingShortUri = resolvingUris.includes(shortUri); isResolvingShortUri = resolvingUris.includes(shortUri);
} }
@ -3376,13 +3442,21 @@ from, isBackgroundSearch = false) => (dispatch, getState) => {
const actions = []; const actions = [];
data.forEach(result => { data.forEach(result => {
if (result.name) { if (result) {
const uri = buildURI({ const { name, claimId } = result;
claimName: result.name, const urlObj = {};
claimId: result.claimId
}); if (name.startsWith('@')) {
actions.push(doResolveUri(uri)); urlObj.channelName = name;
uris.push(uri); 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 byId = Object.assign({}, state.byId);
const channelClaimCounts = Object.assign({}, state.channelClaimCounts); const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
Object.entries(resolveInfo).forEach(([uri, resolveResponse]) => { Object.entries(resolveInfo).forEach(([url, resolveResponse]) => {
// $FlowFixMe // $FlowFixMe
if (resolveResponse.claimsInChannel) { const { claimsInChannel, stream, channel } = resolveResponse;
// $FlowFixMe if (claimsInChannel) {
channelClaimCounts[uri] = resolveResponse.claimsInChannel; channelClaimCounts[url] = claimsInChannel;
} }
});
// $FlowFixMe
Object.entries(resolveInfo).forEach(([uri, { channel, stream }]) => {
if (stream) { if (stream) {
byId[stream.claim_id] = stream; byId[stream.claim_id] = stream;
byUri[uri] = stream.claim_id; byUri[url] = stream.claim_id;
} }
if (channel) { if (channel) {
byId[channel.claim_id] = 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) { 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 fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery);
const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery); const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery);
const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached); const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached);
const { append, query, uris, pageSize } = action.data; const { append, query, urls, pageSize } = action.data;
if (append) { 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] = claimSearchByQuery[query] && claimSearchByQuery[query].length ? claimSearchByQuery[query].concat(urls) : urls;
} else { } 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 // the returned number of urls is less than the page size, so we're on the last page
claimSearchByQueryLastPageReached[query] = uris.length < pageSize; claimSearchByQueryLastPageReached[query] = urls.length < pageSize;
delete fetchingClaimSearchByQuery[query]; 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; }; 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 = { const defaultState$6 = {
editingURI: undefined, editingURI: undefined,
@ -4282,7 +4355,7 @@ const publishReducer = handleActions({
publishSuccess: true publishSuccess: true
}), }),
[DO_PREPARE_EDIT]: (state, action) => { [DO_PREPARE_EDIT]: (state, action) => {
const publishData = _objectWithoutProperties$2(action.data, []); const publishData = _objectWithoutProperties$3(action.data, []);
const { channel, name, uri } = publishData; const { channel, name, uri } = publishData;
// The short uri is what is presented to the user // The short uri is what is presented to the user
@ -4892,6 +4965,7 @@ exports.isNameValid = isNameValid;
exports.isURIClaimable = isURIClaimable; exports.isURIClaimable = isURIClaimable;
exports.isURIValid = isURIValid; exports.isURIValid = isURIValid;
exports.makeSelectAmountForUri = makeSelectAmountForUri; exports.makeSelectAmountForUri = makeSelectAmountForUri;
exports.makeSelectCanonicalUrlForUri = makeSelectCanonicalUrlForUri;
exports.makeSelectChannelForClaimUri = makeSelectChannelForClaimUri; exports.makeSelectChannelForClaimUri = makeSelectChannelForClaimUri;
exports.makeSelectClaimForUri = makeSelectClaimForUri; exports.makeSelectClaimForUri = makeSelectClaimForUri;
exports.makeSelectClaimIsMine = makeSelectClaimIsMine; exports.makeSelectClaimIsMine = makeSelectClaimIsMine;

View file

@ -23,6 +23,7 @@ declare type GenericClaim = {
decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044
timestamp?: number, // date of last transaction timestamp?: number, // date of last transaction
height: number, // block height the tx was confirmed height: number, // block height the tx was confirmed
is_mine: boolean,
name: string, name: string,
normalized_name: string, // `name` normalized via unicode NFD spec, normalized_name: string, // `name` normalized via unicode NFD spec,
nout: number, // index number for an output of a tx nout: number, // index number for an output of a tx

View file

@ -65,7 +65,7 @@ declare type VersionResponse = {
declare type ResolveResponse = { declare type ResolveResponse = {
// Keys are the url(s) passed to resolve // 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 }; declare type GetResponse = FileListItem & { error?: string };

2
dist/flow-typed/i18n.js vendored Normal file
View file

@ -0,0 +1,2 @@
// @flow
declare function __(a: string, b?: string | number): string;

1
flow-typed/Claim.js vendored
View file

@ -23,6 +23,7 @@ declare type GenericClaim = {
decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044
timestamp?: number, // date of last transaction timestamp?: number, // date of last transaction
height: number, // block height the tx was confirmed height: number, // block height the tx was confirmed
is_mine: boolean,
name: string, name: string,
normalized_name: string, // `name` normalized via unicode NFD spec, normalized_name: string, // `name` normalized via unicode NFD spec,
nout: number, // index number for an output of a tx nout: number, // index number for an output of a tx

2
flow-typed/Lbry.js vendored
View file

@ -65,7 +65,7 @@ declare type VersionResponse = {
declare type ResolveResponse = { declare type ResolveResponse = {
// Keys are the url(s) passed to resolve // 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 }; declare type GetResponse = FileListItem & { error?: string };

2
flow-typed/i18n.js vendored Normal file
View file

@ -0,0 +1,2 @@
// @flow
declare function __(a: string, b?: string | number): string;

View file

@ -172,6 +172,7 @@ export {
makeSelectPendingByUri, makeSelectPendingByUri,
makeSelectClaimsInChannelForCurrentPageState, makeSelectClaimsInChannelForCurrentPageState,
makeSelectShortUrlForUri, makeSelectShortUrlForUri,
makeSelectCanonicalUrlForUri,
makeSelectSupportsForUri, makeSelectSupportsForUri,
selectPendingById, selectPendingById,
selectClaimsById, selectClaimsById,

View file

@ -1,46 +1,77 @@
// @flow
const isProduction = process.env.NODE_ENV === 'production';
const channelNameMinLength = 1; const channelNameMinLength = 1;
const claimIdMaxLength = 40; const claimIdMaxLength = 40;
// see https://spec.lbry.com/#urls // 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 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}$/; 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 * Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names. * 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: * Returns a dictionary with keys:
* - name (string): The value in the "name" position in the URI. Note that this * - path (string)
* 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)
* - isChannel (boolean) * - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path * - streamName (string, if present)
* - channelName (string, if present): Channel name without @ * - 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 // Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp( const componentsRegex = new RegExp(
'^((?:lbry://)?)' + // protocol regexPartProtocol + // protocol
'([^:$#/]*)' + // claim name (stops at the first separator or end) regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path '(/?)' + // 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 // Validate protocol
if (requireProto && !proto) { if (requireProto && !proto) {
@ -48,14 +79,15 @@ export function parseURI(URI, requireProto = false) {
} }
// Validate and process name // Validate and process name
if (!claimName) { if (!streamNameOrChannelName) {
throw new Error(__('URI does not include name.')); throw new Error(__('URI does not include name.'));
} }
const isChannel = claimName.startsWith('@'); const includesChannel = streamNameOrChannelName.startsWith('@');
const channelName = isChannel ? claimName.slice(1) : claimName; const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
const channelName = includesChannel && streamNameOrChannelName.slice(1);
if (isChannel) { if (includesChannel) {
if (!channelName) { if (!channelName) {
throw new Error(__('No channel name after @.')); throw new Error(__('No channel name after @.'));
} }
@ -63,36 +95,58 @@ export function parseURI(URI, requireProto = false) {
if (channelName.length < channelNameMinLength) { if (channelName.length < channelNameMinLength) {
throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength)); throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength));
} }
contentName = path;
} }
const nameBadChars = (channelName || claimName).match(regexInvalidURI); // Validate and process modifier
if (nameBadChars) { const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(
throw new Error( primaryModSeparator,
__( primaryModValue
`Invalid character %s in name: %s.`,
nameBadChars.length === 1 ? '' : 's',
nameBadChars.join(', ')
)
); );
} 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 claimId;
let claimSequence; let claimSequence;
let bidPosition; let bidPosition;
if (modSep) { if (modSeperator) {
if (!modVal) { if (!modValue) {
throw new Error(__(`No modifier provided after separator %s.`, modSep)); throw new Error(__(`No modifier provided after separator %s.`, modSeperator));
} }
if (modSep === '#') { if (modSeperator === '#') {
claimId = modVal; claimId = modValue;
} else if (modSep === ':') { } else if (modSeperator === ':') {
claimSequence = modVal; claimSequence = modValue;
} else if (modSep === '$') { } else if (modSeperator === '$') {
bidPosition = modVal; bidPosition = modValue;
} }
} }
@ -108,33 +162,7 @@ export function parseURI(URI, requireProto = false) {
throw new Error(__('Bid position must be a number.')); throw new Error(__('Bid position must be a number.'));
} }
// Validate and process path return [claimId, claimSequence, bidPosition];
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 } : {}),
};
} }
/** /**
@ -142,91 +170,144 @@ export function parseURI(URI, requireProto = false) {
* *
* The channelName key will accept names with or without the @ prefix. * The channelName key will accept names with or without the @ prefix.
*/ */
export function buildURI(URIObj, includeProto = true, protoDefault = 'lbry://') { export function buildURI(
const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj; 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 (!isProduction) {
if (claimId) {
if (channelName) { console.error(
const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`; __("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")
if (!claimName) { );
claimName = channelNameFormatted; }
} else if (claimName !== channelNameFormatted) { if (claimName) {
throw new Error( 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) { if (contentName) {
if (!claimName) { console.error(__("'contentName' should no longer be used. Use 'streamName' instead"));
claimName = contentName;
} else if (!path) {
path = contentName;
} }
if (path && path !== contentName) { }
if (!claimName && !channelName && !streamName) {
throw new Error( throw new Error(
__( __(
'Path and contentName do not match. Only one is required; most likely you wanted contentName.' "'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 ( return (
(includeProto ? protoDefault : '') + (includeProto ? protoDefault : '') +
claimName + // primaryClaimName will always exist here because we throw above if there is no "name" value passed in
(claimId ? `#${claimId}` : '') + // $FlowFixMe
(claimSequence ? `:${claimSequence}` : '') + primaryClaimName +
(bidPosition ? `${bidPosition}` : '') + (primaryClaimId ? `#${primaryClaimId}` : '') +
(path ? `/${path}` : '') (primaryClaimSequence ? `:${primaryClaimSequence}` : '') +
(primaryBidPosition ? `${primaryBidPosition}` : '') +
(secondaryClaimName ? `/${secondaryClaimName}` : '') +
(secondaryClaimId ? `#${secondaryClaimId}` : '') +
(secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') +
(secondaryBidPosition ? `${secondaryBidPosition}` : '')
); );
} }
/* Takes a parseable LBRY URI and converts it to standard, canonical format */ /* Takes a parseable LBRY URL and converts it to standard, canonical format */
export function normalizeURI(URI) { export function normalizeURI(URL: string) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
return buildURI({ claimName, path, claimSequence, bidPosition, claimId }); streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
} = parseURI(URL);
return buildURI({
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
});
} }
export function isURIValid(URI) { export function isURIValid(URL: string): boolean {
let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.claimName;
return true;
} }
export function isNameValid(claimName) { export function isNameValid(claimName: string) {
return !regexInvalidURI.test(claimName); return !regexInvalidURI.test(claimName);
} }
export function isURIClaimable(URI) { export function isURIClaimable(URL: string) {
let parts; let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parts = parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return (
parts && return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
parts.claimName &&
!parts.claimId &&
!parts.bidPosition &&
!parts.claimSequence &&
!parts.isChannel &&
!parts.path
);
} }
export function convertToShareLink(URI) { export function convertToShareLink(URL: string) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryBidPosition,
primaryClaimSequence,
secondaryBidPosition,
secondaryClaimSequence,
} = parseURI(URL);
return buildURI( return buildURI(
{ claimName, path, claimSequence, bidPosition, claimId }, {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryBidPosition,
primaryClaimSequence,
secondaryBidPosition,
secondaryClaimSequence,
},
true, true,
'https://open.lbry.com/' 'https://open.lbry.com/'
); );

View file

@ -328,10 +328,10 @@ export function doClaimSearch(
const success = (data: ClaimSearchResponse) => { const success = (data: ClaimSearchResponse) => {
const resolveInfo = {}; const resolveInfo = {};
const uris = []; const urls = [];
data.items.forEach((stream: Claim) => { data.items.forEach((stream: Claim) => {
resolveInfo[stream.permanent_url] = { stream }; resolveInfo[stream.canonical_url] = { stream };
uris.push(stream.permanent_url); urls.push(stream.canonical_url);
}); });
dispatch({ dispatch({
@ -339,7 +339,7 @@ export function doClaimSearch(
data: { data: {
query, query,
resolveInfo, resolveInfo,
uris, urls,
append: options.page && options.page !== 1, append: options.page && options.page !== 1,
pageSize: options.page_size, pageSize: options.page_size,
}, },

View file

@ -112,18 +112,26 @@ export const doSearch = (
fetch(`${CONNECTION_STRING}search?${queryWithOptions}`) fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
.then(handleFetchResponse) .then(handleFetchResponse)
.then((data: Array<{ name: String, claimId: string }>) => { .then((data: Array<{ name: string, claimId: string }>) => {
const uris = []; const uris = [];
const actions = []; const actions = [];
data.forEach(result => { data.forEach(result => {
if (result.name) { if (result) {
const uri = buildURI({ const { name, claimId } = result;
claimName: result.name, const urlObj = {};
claimId: result.claimId,
}); if (name.startsWith('@')) {
actions.push(doResolveUri(uri)); urlObj.channelName = name;
uris.push(uri); urlObj.channelClaimId = claimId;
} else {
urlObj.streamName = name;
urlObj.streamClaimId = claimId;
}
const url = buildURI(urlObj);
actions.push(doResolveUri(url));
uris.push(url);
} }
}); });

View file

@ -66,26 +66,25 @@ function handleClaimAction(state: State, action: any): State {
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
const channelClaimCounts = Object.assign({}, state.channelClaimCounts); const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
Object.entries(resolveInfo).forEach(([uri: string, resolveResponse: Claim]) => { Object.entries(resolveInfo).forEach(([url: string, resolveResponse: ResolveResponse]) => {
// $FlowFixMe // $FlowFixMe
if (resolveResponse.claimsInChannel) { const { claimsInChannel, stream, channel } = resolveResponse;
// $FlowFixMe if (claimsInChannel) {
channelClaimCounts[uri] = resolveResponse.claimsInChannel; channelClaimCounts[url] = claimsInChannel;
} }
});
// $FlowFixMe
Object.entries(resolveInfo).forEach(([uri, { channel, stream }]) => {
if (stream) { if (stream) {
byId[stream.claim_id] = stream; byId[stream.claim_id] = stream;
byUri[uri] = stream.claim_id; byUri[url] = stream.claim_id;
} }
if (channel) { if (channel) {
byId[channel.claim_id] = 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) { 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 state.claimSearchByQueryLastPageReached
); );
const { append, query, uris, pageSize } = action.data; const { append, query, urls, pageSize } = action.data;
if (append) { if (append) {
// todo: check for duplicate uris when concatenating? // todo: check for duplicate urls when concatenating?
claimSearchByQuery[query] = claimSearchByQuery[query] =
claimSearchByQuery[query] && claimSearchByQuery[query].length claimSearchByQuery[query] && claimSearchByQuery[query].length
? claimSearchByQuery[query].concat(uris) ? claimSearchByQuery[query].concat(urls)
: uris; : urls;
} else { } 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 // the returned number of urls is less than the page size, so we're on the last page
claimSearchByQueryLastPageReached[query] = uris.length < pageSize; claimSearchByQueryLastPageReached[query] = urls.length < pageSize;
delete fetchingClaimSearchByQuery[query]; delete fetchingClaimSearchByQuery[query];

View file

@ -62,8 +62,10 @@ export const makeSelectClaimIsPending = (uri: string) =>
selectPendingById, selectPendingById,
pendingById => { pendingById => {
let claimId; let claimId;
try { try {
({ claimId } = parseURI(uri)); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
claimId = isChannel ? channelClaimId : streamClaimId;
} catch (e) {} } catch (e) {}
if (claimId) { if (claimId) {
@ -76,7 +78,8 @@ export const makeSelectPendingByUri = (uri: string) =>
createSelector( createSelector(
selectPendingById, selectPendingById,
pendingById => { pendingById => {
const { claimId } = parseURI(uri); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
const claimId = isChannel ? channelClaimId : streamClaimId;
return pendingById[claimId]; return pendingById[claimId];
} }
); );
@ -90,13 +93,16 @@ export const makeSelectClaimForUri = (uri: string) =>
// It won't be in claimsByUri because resolving it will return nothing // It won't be in claimsByUri because resolving it will return nothing
let valid; let valid;
let claimId; let channelClaimId;
let streamClaimId;
let isChannel;
try { try {
({ claimId } = parseURI(uri)); ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri));
valid = true; valid = true;
} catch (e) {} } catch (e) {}
if (valid) { if (valid) {
const claimId = isChannel ? channelClaimId : streamClaimId;
const pendingClaim = pendingById[claimId]; const pendingClaim = pendingById[claimId];
if (pendingClaim) { if (pendingClaim) {
@ -521,23 +527,18 @@ export const selectClaimSearchByQueryLastPageReached = createSelector(
state => state.claimSearchByQueryLastPageReached || {} 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) => export const makeSelectShortUrlForUri = (uri: string) =>
createSelector( createSelector(
makeSelectClaimForUri(uri), makeSelectClaimForUri(uri),
claim => claim && claim.short_url claim => claim && claim.short_url
); );
export const makeSelectCanonicalUrlForUri = (uri: string) =>
createSelector(
makeSelectClaimForUri(uri),
claim => claim && claim.canonical_url
);
export const makeSelectSupportsForUri = (uri: string) => export const makeSelectSupportsForUri = (uri: string) =>
createSelector( createSelector(
selectSupportsByOutpoint, selectSupportsByOutpoint,
@ -548,11 +549,12 @@ export const makeSelectSupportsForUri = (uri: string) =>
} }
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
let total = parseFloat("0.0"); let total = 0;
Object.values(byOutpoint).forEach(support => { Object.values(byOutpoint).forEach(support => {
const { claim_id, amount } = support // $FlowFixMe
total = (claim_id === claimId && amount) ? total + parseFloat(amount) : total; const { claim_id, amount } = support;
total = claim_id === claimId && amount ? total + parseFloat(amount) : total;
}); });
return total; return total;

View file

@ -33,8 +33,16 @@ export const selectIsStillEditing = createSelector(
return false; return false;
} }
const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); const {
const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); 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 // 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 // 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 return isStillEditing
? claimsById[editClaimId] ? claimsById[editClaimId]
: myClaims.find(claim => : 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; let isResolvingShortUri;
if (isChannel) { if (isChannel) {
const shortUri = buildURI({ contentName: name }); const shortUri = buildURI({ streamName: name });
isResolvingShortUri = resolvingUris.includes(shortUri); isResolvingShortUri = resolvingUris.includes(shortUri);
} }

View file

@ -77,15 +77,15 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
let searchSuggestions = []; let searchSuggestions = [];
try { try {
const uri = normalizeURI(query); const uri = normalizeURI(query);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
searchSuggestions.push( searchSuggestions.push(
{ {
value: claimName, value: streamName,
type: SEARCH_TYPES.SEARCH, type: SEARCH_TYPES.SEARCH,
}, },
{ {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
} }
); );
@ -110,11 +110,11 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
// determine if it's a channel // determine if it's a channel
try { try {
const uri = normalizeURI(suggestion); const uri = normalizeURI(suggestion);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
return { return {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
}; };
} catch (e) { } catch (e) {

View file

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