diff --git a/CHANGELOG.md b/CHANGELOG.md index 2002e1aad..ced55e42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * New welcome flow for new users * Redesigned UI for Discover * Handle more of price calculations at the daemon layer to improve page load time + * Add special support for building channel claims in lbryuri module ### Changed * Update process now easier and more reliable diff --git a/ui/js/component/channel-indicator.js b/ui/js/component/channel-indicator.js index db2f40995..e19850c28 100644 --- a/ui/js/component/channel-indicator.js +++ b/ui/js/component/channel-indicator.js @@ -19,6 +19,7 @@ const UriIndicator = React.createClass({ const channelUriObj = Object.assign({}, uriObj); delete channelUriObj.path; + delete channelUriObj.contentName; const channelUri = lbryuri.build(channelUriObj, false); let icon, modifier; diff --git a/ui/js/lbryuri.js b/ui/js/lbryuri.js index 2a6009cd7..55a964e66 100644 --- a/ui/js/lbryuri.js +++ b/ui/js/lbryuri.js @@ -7,14 +7,23 @@ const lbryuri = {}; * 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) - * - properName (string; strips off @ for channels) - * - isChannel (boolean) + * - 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, if persent) + * - isChannel (boolean) + * - contentName (string): For anon claims, the name; for channel claims, the path + * - channelName (string, if present): Channel name without @ */ lbryuri.parse = function(uri, requireProto=false) { // Break into components. Empty sub-matches are converted to null @@ -26,6 +35,8 @@ lbryuri.parse = function(uri, requireProto=false) { ); const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null); + let contentName; + // Validate protocol if (requireProto && !proto) { throw new Error('LBRY URIs must include a protocol prefix (lbry://).'); @@ -36,20 +47,22 @@ lbryuri.parse = function(uri, requireProto=false) { throw new Error('URI does not include name.'); } - const isChannel = name[0] == '@'; - const properName = isChannel ? name.substr(1) : name; + const isChannel = name.startsWith('@'); + const channelName = isChannel ? name.slice(1) : name; if (isChannel) { - if (!properName) { + if (!channelName) { throw new Error('No channel name after @.'); } - if (properName.length < CHANNEL_NAME_MIN_LEN) { + if (channelName.length < CHANNEL_NAME_MIN_LEN) { throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`); } + + contentName = path; } - const nameBadChars = properName.match(/[^A-Za-z0-9-]/g); + const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g); if (nameBadChars) { throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`); } @@ -82,7 +95,7 @@ lbryuri.parse = function(uri, requireProto=false) { throw new Error('Bid position must be a number.'); } - // Validate path + // Validate and process path if (path) { if (!isChannel) { throw new Error('Only channel URIs may have a path.'); @@ -92,12 +105,16 @@ lbryuri.parse = function(uri, requireProto=false) { if (pathBadChars) { throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`); } + + contentName = path; } else if (pathSep) { throw new Error('No path provided after /'); } return { - name, properName, isChannel, + name, path, isChannel, + ... contentName ? {contentName} : {}, + ... channelName ? {channelName} : {}, ... claimSequence ? {claimSequence: parseInt(claimSequence)} : {}, ... bidPosition ? {bidPosition: parseInt(bidPosition)} : {}, ... claimId ? {claimId} : {}, @@ -105,20 +122,45 @@ lbryuri.parse = function(uri, requireProto=false) { }; } -lbryuri.build = function(uriObj, includeProto=true) { - const {name, claimId, claimSequence, bidPosition, path} = uriObj; +/** + * Takes an object in the same format returned by lbryuri.parse() and builds a URI. + * + * The channelName key will accept names with or without the @ prefix. + */ +lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) { + let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj; + + if (channelName) { + const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName; + if (!name) { + name = channelNameFormatted; + } else if (name !== channelNameFormatted) { + throw new Error('Received a channel content URI, but 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 (contentName) { + if (!path) { + path = contentName; + } else if (path !== contentName) { + throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.'); + } + } return (includeProto ? 'lbry://' : '') + name + (claimId ? `#${claimId}` : '') + (claimSequence ? `:${claimSequence}` : '') + (bidPosition ? `\$${bidPosition}` : '') + (path ? `/${path}` : ''); + } /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just - * consists of making sure it has a lbry:// prefix) */ + * consists of adding the lbry:// prefix if needed) */ lbryuri.normalize= function(uri) { - return lbryuri.build(lbryuri.parse(uri)); + const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri); + return lbryuri.build({name, path, claimSequence, bidPosition, claimId}); } +window.lbryuri = lbryuri; export default lbryuri; diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 671f418fe..dc2811cfd 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -47,19 +47,11 @@ var SearchResults = React.createClass({ var rows = [], seenNames = {}; //fix this when the search API returns claim IDs for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { - let uri; - if (channel_name) { - uri = lbryuri.build({ - name: channel_name, - path: name, - claimId: channel_id, - }); - } else { - uri = lbryuri.build({ - name: name, - claimId: claim_id, - }) - } + const uri = lbryuri.build({ + channelName: channel_name, + contentName: name, + claimId: channel_id || claim_id, + }); rows.push( diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 1fc8ca5b9..063730e7f 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -196,12 +196,7 @@ export let FileList = React.createClass({ } - let uri; - if (!channel_name) { - uri = lbryuri.build({name}); - } else { - uri = lbryuri.build({name: channel_name, path: name}); - } + const uri = lbryuri.build({contentName: name, channelName: channel_name}); seenUris[name] = true; content.push(