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 2f30d5755..e19850c28 100644 --- a/ui/js/component/channel-indicator.js +++ b/ui/js/component/channel-indicator.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Icon} from './common.js'; const UriIndicator = React.createClass({ @@ -11,7 +11,7 @@ const UriIndicator = React.createClass({ }, render: function() { - const uriObj = uri.parseLbryUri(this.props.uri); + const uriObj = lbryuri.parse(this.props.uri); if (!this.props.hasSignature || !uriObj.isChannel) { return Anonymous; @@ -19,7 +19,8 @@ const UriIndicator = React.createClass({ const channelUriObj = Object.assign({}, uriObj); delete channelUriObj.path; - const channelUri = uri.buildLbryUri(channelUriObj, false); + delete channelUriObj.contentName; + const channelUri = lbryuri.build(channelUriObj, false); let icon, modifier; if (this.props.signatureIsValid) { diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 6c105ed75..21d25eb47 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Link} from '../component/link.js'; import {Icon, FilePrice} from '../component/common.js'; import {Modal} from './modal.js'; @@ -156,8 +156,8 @@ let FileActionsRow = React.createClass({ linkBlock = ; } - const lbryUri = uri.normalizeLbryUri(this.props.uri); - const title = this.props.metadata ? this.props.metadata.title : lbryUri; + const uri = lbryuri.normalize(this.props.uri); + const title = this.props.metadata ? this.props.metadata.title : uri; return (
{this.state.fileInfo !== null || this.state.fileInfo.isMine @@ -170,7 +170,7 @@ let FileActionsRow = React.createClass({ : '' } - Are you sure you'd like to buy {title} for credits? + Are you sure you'd like to buy {title} for credits? @@ -178,7 +178,7 @@ let FileActionsRow = React.createClass({ - LBRY was unable to download the stream {lbryUri}. + LBRY was unable to download the stream {uri}.
- +
{ !this.props.hidePrice ? : null} - +

- + {title} @@ -184,12 +184,12 @@ export let FileCardStream = React.createClass({ return null; } - const lbryUri = uri.normalizeLbryUri(this.props.uri); + const uri = lbryuri.normalize(this.props.uri); const metadata = this.props.metadata; const isConfirmed = !!metadata; - const title = isConfirmed ? metadata.title : lbryUri; + const title = isConfirmed ? metadata.title : uri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; - const primaryUrl = '?show=' + lbryUri; + const primaryUrl = '?show=' + uri; return (
@@ -198,7 +198,7 @@ export let FileCardStream = React.createClass({
{title}
{ !this.props.hidePrice ? : null} -
diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 244b82142..adc8ba4b4 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,7 +1,7 @@ import lbryio from './lbryio.js'; import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.js'; -import uri from './uri.js'; +import lbryuri from './lbryuri.js'; import {getLocal, getSession, setSession, setLocal} from './utils.js'; const {remote} = require('electron'); @@ -12,19 +12,19 @@ const menu = remote.require('./menu/main-menu'); * needed to make a dummy claim or file info object. */ function savePendingPublish({name, channel_name}) { - let lbryUri; + let uri; if (channel_name) { - lbryUri = uri.buildLbryUri({name: channel_name, path: name}, false); + uri = lbryuri.build({name: channel_name, path: name}, false); } else { - lbryUri = uri.buildLbryUri({name: name}, false); + uri = lbryuri.build({name: name}, false); } const pendingPublishes = getLocal('pendingPublishes') || []; const newPendingPublish = { name, channel_name, - claim_id: 'pending_claim_' + lbryUri, - txid: 'pending_' + lbryUri, + claim_id: 'pending_claim_' + uri, + txid: 'pending_' + uri, nout: 0, - outpoint: 'pending_' + lbryUri + ':0', + outpoint: 'pending_' + uri + ':0', time: Date.now(), }; setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); @@ -215,35 +215,35 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { * from Lighthouse is included. */ lbry.costPromiseCache = {} -lbry.getCostInfo = function(lbryUri) { - if (lbry.costPromiseCache[lbryUri] === undefined) { - lbry.costPromiseCache[lbryUri] = new Promise((resolve, reject) => { +lbry.getCostInfo = function(uri) { + if (lbry.costPromiseCache[uri] === undefined) { + lbry.costPromiseCache[uri] = new Promise((resolve, reject) => { const COST_INFO_CACHE_KEY = 'cost_info_cache'; let costInfoCache = getSession(COST_INFO_CACHE_KEY, {}) function cacheAndResolve(cost, includesData) { - costInfoCache[lbryUri] = {cost, includesData}; + costInfoCache[uri] = {cost, includesData}; setSession(COST_INFO_CACHE_KEY, costInfoCache); resolve({cost, includesData}); } - if (!lbryUri) { + if (!uri) { return reject(new Error(`URI required.`)); } - if (costInfoCache[lbryUri] && costInfoCache[lbryUri].cost) { - return resolve(costInfoCache[lbryUri]) + if (costInfoCache[uri] && costInfoCache[uri].cost) { + return resolve(costInfoCache[uri]) } - function getCost(lbryUri, size) { - lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { + function getCost(uri, size) { + lbry.stream_cost_estimate({uri, ... size !== null ? {size} : {}}).then((cost) => { cacheAndResolve(cost, size !== null); }, reject); } - function getCostGenerous(lbryUri) { + function getCostGenerous(uri) { // If generous is on, the calculation is simple enough that we might as well do it here in the front end - lbry.resolve({uri: lbryUri}).then((resolutionInfo) => { + lbry.resolve({uri: uri}).then((resolutionInfo) => { const fee = resolutionInfo.claim.value.stream.metadata.fee; if (fee === undefined) { cacheAndResolve(0, true); @@ -257,12 +257,12 @@ lbry.getCostInfo = function(lbryUri) { }); } - const uriObj = uri.parseLbryUri(lbryUri); + const uriObj = lbryuri.parse(uri); const name = uriObj.path || uriObj.name; lbry.settings_get({allow_cached: true}).then(({is_generous_host}) => { if (is_generous_host) { - return getCostGenerous(lbryUri); + return getCostGenerous(uri); } lighthouse.get_size_for_name(name).then((size) => { @@ -278,7 +278,7 @@ lbry.getCostInfo = function(lbryUri) { }); }); } - return lbry.costPromiseCache[lbryUri]; + return lbry.costPromiseCache[uri]; } lbry.getMyClaims = function(callback) { diff --git a/ui/js/uri.js b/ui/js/lbryuri.js similarity index 54% rename from ui/js/uri.js rename to ui/js/lbryuri.js index 67615f7b5..55a964e66 100644 --- a/ui/js/uri.js +++ b/ui/js/lbryuri.js @@ -1,22 +1,31 @@ const CHANNEL_NAME_MIN_LEN = 4; const CLAIM_ID_MAX_LEN = 40; -const uri = {}; +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 @ */ -uri.parseLbryUri = function(lbryUri, requireProto=false) { +lbryuri.parse = function(uri, requireProto=false) { // Break into components. Empty sub-matches are converted to null const componentsRegex = new RegExp( '^((?:lbry:\/\/)?)' + // protocol @@ -24,7 +33,9 @@ uri.parseLbryUri = function(lbryUri, requireProto=false) { '([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) '(/?)(.*)' // path separator, path ); - const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(lbryUri).slice(1).map(match => match || null); + const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null); + + let contentName; // Validate protocol if (requireProto && !proto) { @@ -36,20 +47,22 @@ uri.parseLbryUri = function(lbryUri, 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 @@ uri.parseLbryUri = function(lbryUri, 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 @@ uri.parseLbryUri = function(lbryUri, 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 @@ uri.parseLbryUri = function(lbryUri, requireProto=false) { }; } -uri.buildLbryUri = 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) */ -uri.normalizeLbryUri = function(lbryUri) { - return uri.buildLbryUri(uri.parseLbryUri(lbryUri)); + * consists of adding the lbry:// prefix if needed) */ +lbryuri.normalize= function(uri) { + const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri); + return lbryuri.build({name, path, claimSequence, bidPosition, claimId}); } -export default uri; +window.lbryuri = lbryuri; +export default lbryuri; diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index fd0f25811..dc2811cfd 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -1,7 +1,7 @@ import React from 'react'; import lbry from '../lbry.js'; import lbryio from '../lbryio.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import lighthouse from '../lighthouse.js'; import {FileTile, FileTileStream} from '../component/file-tile.js'; import {Link} from '../component/link.js'; @@ -47,22 +47,14 @@ 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 lbryUri; - if (channel_name) { - lbryUri = uri.buildLbryUri({ - name: channel_name, - path: name, - claimId: channel_id, - }); - } else { - lbryUri = uri.buildLbryUri({ - name: name, - claimId: claim_id, - }) - } + const uri = lbryuri.build({ + channelName: channel_name, + contentName: name, + claimId: channel_id || claim_id, + }); rows.push( - + ); } return ( diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index e746f9d77..063730e7f 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Link} from '../component/link.js'; import {FormField} from '../component/form.js'; import {FileTileStream} from '../component/file-tile.js'; @@ -196,14 +196,9 @@ export let FileList = React.createClass({ } - let fileUri; - if (!channel_name) { - fileUri = uri.buildLbryUri({name}); - } else { - fileUri = uri.buildLbryUri({name: channel_name, path: name}); - } + const uri = lbryuri.build({contentName: name, channelName: channel_name}); seenUris[name] = true; - content.push(); } diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index b424e07ee..13736f0db 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -1,6 +1,5 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; import {FormField, FormRow} from '../component/form.js'; import {Link} from '../component/link.js'; import rewards from '../rewards.js'; diff --git a/ui/js/page/show.js b/ui/js/page/show.js index ea41731af..cc2fb5cfc 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -1,7 +1,7 @@ import React from 'react'; import lbry from '../lbry.js'; import lighthouse from '../lighthouse.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Video} from '../page/watch.js' import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js'; import {FileActions} from '../component/file-actions.js'; @@ -59,7 +59,7 @@ let ShowPage = React.createClass({ }; }, componentWillMount: function() { - this._uri = uri.normalizeLbryUri(this.props.uri); + this._uri = lbryuri.normalize(this.props.uri); document.title = this._uri; lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {