diff --git a/client/lib/url/index.js b/client/lib/url/index.js index 0d69a60bca9926..4711909d9756bc 100644 --- a/client/lib/url/index.js +++ b/client/lib/url/index.js @@ -3,6 +3,7 @@ */ import { parse as parseUrl } from 'url'; import { startsWith } from 'lodash'; +import url from 'url'; /** * Internal dependencies @@ -107,6 +108,41 @@ function urlToSlug( url ) { return withoutHttp( url ).replace( /\//g, '::' ); } +/** + * Checks if the supplied string appears to be a URL. + * Looks only for the absolute basics: + * - does it have a .suffix? + * - does it have at least two parts separated by a dot? + * + * @param {String} query The string to check + * @return {Boolean} Does it appear to be a URL? + */ +function resemblesUrl( query ) { + let parsedUrl = url.parse( query ); + + // Make sure the query has a protocol - hostname ends up blank otherwise + if ( ! parsedUrl.protocol ) { + parsedUrl = url.parse( 'http://' + query ); + } + + if ( ! parsedUrl.hostname || parsedUrl.hostname.indexOf( '.' ) === -1 ) { + return false; + } + + // Check for a valid-looking TLD + if ( parsedUrl.hostname.lastIndexOf( '.' ) > ( parsedUrl.hostname.length - 3 ) ) { + return false; + } + + // Make sure the hostname has at least two parts separated by a dot + const hostnameParts = parsedUrl.hostname.split( '.' ).filter( Boolean ); + if ( hostnameParts.length < 2 ) { + return false; + } + + return true; +} + export default { isOutsideCalypso, isExternal, @@ -116,5 +152,6 @@ export default { setUrlScheme, urlToSlug, // [TODO]: Move lib/route/add-query-args contents here - addQueryArgs + addQueryArgs, + resemblesUrl, }; diff --git a/client/lib/url/test/index.js b/client/lib/url/test/index.js index fe19136f9315fb..d2ddc5e246eec7 100644 --- a/client/lib/url/test/index.js +++ b/client/lib/url/test/index.js @@ -13,6 +13,7 @@ import { addSchemeIfMissing, setUrlScheme, urlToSlug, + resemblesUrl, } from '../'; describe( 'withoutHttp', () => { @@ -285,3 +286,45 @@ describe( 'urlToSlug()', () => { expect( urlWithoutHttp ).to.equal( 'example.com::example::test123' ); } ); } ); + +describe( 'resemblesUrl()', () => { + it( 'should detect a URL', () => { + const source = 'http://example.com/path'; + expect( resemblesUrl( source ) ).to.equal( true ); + } ); + + it( 'should detect a URL without protocol', () => { + const source = 'example.com'; + expect( resemblesUrl( source ) ).to.equal( true ); + } ); + + it( 'should detect a URL with a query string', () => { + const source = 'http://example.com/path?query=banana&query2=pineapple'; + expect( resemblesUrl( source ) ).to.equal( true ); + } ); + + it( 'should detect a URL with a short suffix', () => { + const source = 'http://example.cc'; + expect( resemblesUrl( source ) ).to.equal( true ); + } ); + + it( 'should return false with adjacent dots', () => { + const source = '..com'; + expect( resemblesUrl( source ) ).to.equal( false ); + } ); + + it( 'should return false with spaced dots', () => { + const source = '. . .com'; + expect( resemblesUrl( source ) ).to.equal( false ); + } ); + + it( 'should return false with a single dot', () => { + const source = '.'; + expect( resemblesUrl( source ) ).to.equal( false ); + } ); + + it( 'should return false if the string is not a URL', () => { + const source = 'exampledotcom'; + expect( resemblesUrl( source ) ).to.equal( false ); + } ); +} ); diff --git a/client/reader/follow-button/follow-sources.jsx b/client/reader/follow-button/follow-sources.jsx index dddee631e83dc4..9c3ef0f09c632c 100644 --- a/client/reader/follow-button/follow-sources.jsx +++ b/client/reader/follow-button/follow-sources.jsx @@ -5,7 +5,8 @@ const exported = { SEARCH_RESULTS: 'search-results', READER_SUBSCRIPTIONS: 'reader-subscriptions', READER_FEED_SEARCH: 'reader-feed-search-result', - COMBINED_CARD: 'reader-combined-card' + COMBINED_CARD: 'reader-combined-card', + READER_FOLLOWING_MANAGE_URL_INPUT: 'reader-following-manage-url-input', }; export default exported; @@ -16,5 +17,6 @@ export const { SEARCH_RESULTS, READER_SUBSCRIPTIONS, READER_FEED_SEARCH, - COMBINED_CARD + COMBINED_CARD, + READER_FOLLOWING_MANAGE_URL_INPUT, } = exported; diff --git a/client/reader/following-manage/index.jsx b/client/reader/following-manage/index.jsx index 169a4f36a2cbb0..e4ce3dc3979076 100644 --- a/client/reader/following-manage/index.jsx +++ b/client/reader/following-manage/index.jsx @@ -22,6 +22,9 @@ import FollowingManageSearchFeedsResults from './feed-search-results'; import MobileBackToSidebar from 'components/mobile-back-to-sidebar'; import { requestFeedSearch } from 'state/reader/feed-searches/actions'; import { addQueryArgs } from 'lib/url'; +import FollowButton from 'reader/follow-button'; +import { READER_FOLLOWING_MANAGE_URL_INPUT } from 'reader/follow-button/follow-sources'; +import { resemblesUrl, addSchemeIfMissing, withoutHttp } from 'lib/url'; class FollowingManage extends Component { static propTypes = { @@ -94,7 +97,7 @@ class FollowingManage extends Component { this.resizeSearchBox(); // this is a total hack. In React-Virtualized you need to tell a WindowScroller when the things - // above it has moved with a call to updatePosision(). Our issue is we don't have a good moment + // above it has moved with a call to updatePosition(). Our issue is we don't have a good moment // where we know that the content above the WindowScroller has settled down and so instead the solution // here is to call updatePosition in a regular interval. the call takes about 0.1ms from empirical testing. this.updatePosition = setInterval( () => { @@ -129,6 +132,12 @@ class FollowingManage extends Component { showMoreResults } = this.props; const searchPlaceholderText = translate( 'Search millions of sites' ); + const showExistingSubscriptions = ! ( !! sitesQuery && showMoreResults ); + const isSitesQueryUrl = resemblesUrl( sitesQuery ); + let sitesQueryWithoutProtocol; + if ( isSitesQueryUrl ) { + sitesQueryWithoutProtocol = withoutHttp( sitesQuery ); + } return ( @@ -136,7 +145,7 @@ class FollowingManage extends Component {

{ translate( 'Manage Followed Sites' ) }

- { ! searchResults && } + { ! searchResults && ! isSitesQueryUrl && }

{ translate( 'Follow Something New' ) }

@@ -153,8 +162,18 @@ class FollowingManage extends Component { value={ sitesQuery }> + + { isSitesQueryUrl && ( +
+ +
+ ) }
- { !! sitesQuery && ( + { !! sitesQuery && ! isSitesQueryUrl && ( ) } - { ! ( !! sitesQuery && showMoreResults ) && ( + { showExistingSubscriptions && (