diff --git a/client/components/site-icon/index.jsx b/client/components/site-icon/index.jsx
index 0206179e0244a..cfe52c9dec7bd 100644
--- a/client/components/site-icon/index.jsx
+++ b/client/components/site-icon/index.jsx
@@ -3,14 +3,16 @@
*/
import React from 'react';
import { connect } from 'react-redux';
-import { includes } from 'lodash';
+import { get, includes } from 'lodash';
import { parse as parseUrl } from 'url';
import classNames from 'classnames';
/**
* Internal dependencies
*/
+import QuerySites from 'components/data/query-sites';
import { getSite } from 'state/sites/selectors';
+import { getSiteIconUrl } from 'state/selectors';
import resizeImageUrl from 'lib/resize-image-url';
import Gridicon from 'components/gridicon';
@@ -31,28 +33,33 @@ const SiteIcon = React.createClass( {
size: React.PropTypes.number
},
- getIconSrcUrl( imageUrl ) {
- const { host } = parseUrl( imageUrl, true, true );
+ getIconSrcUrl() {
+ const { iconUrl } = this.props;
+ if ( ! iconUrl ) {
+ return;
+ }
+
+ const { host } = parseUrl( iconUrl, true, true );
const sizeParam = includes( host, 'gravatar.com' ) ? 's' : 'w';
- return resizeImageUrl( imageUrl, {
+ return resizeImageUrl( iconUrl, {
[ sizeParam ]: this.props.imgSize
} );
},
render() {
- var iconSrc, iconClasses, style;
+ const { site, siteId } = this.props;
// Set the site icon path if it's available
- iconSrc = ( this.props.site && this.props.site.icon ) ? this.getIconSrcUrl( this.props.site.icon.img ) : null;
+ const iconSrc = this.getIconSrcUrl();
- iconClasses = classNames( {
+ const iconClasses = classNames( {
'site-icon': true,
'is-blank': ! iconSrc
} );
// Size inline styles
- style = {
+ const style = {
height: this.props.size,
width: this.props.size,
lineHeight: this.props.size + 'px',
@@ -61,6 +68,7 @@ const SiteIcon = React.createClass( {
return (
+ { ! site && siteId > 0 &&
}
{ iconSrc
?
:
@@ -70,6 +78,19 @@ const SiteIcon = React.createClass( {
}
} );
-export default connect( ( state, { siteId } ) => (
- siteId ? { site: getSite( state, siteId ) } : {}
-) )( SiteIcon );
+export default connect( ( state, { site, siteId, imgSize } ) => {
+ // Until sites state is completely within Redux, we provide compatibility
+ // in cases where site object is passed to use the icon.img property as URL
+ if ( site ) {
+ return {
+ iconUrl: get( site, 'icon.img' )
+ };
+ }
+
+ // Otherwise, assume we want to perform the lookup in Redux state
+ // exclusively using the site ID
+ return {
+ site: getSite( state, siteId ),
+ iconUrl: getSiteIconUrl( state, siteId, imgSize )
+ };
+} )( SiteIcon );
diff --git a/client/state/selectors/get-media-item.js b/client/state/selectors/get-media-item.js
new file mode 100644
index 0000000000000..8df4270b52c51
--- /dev/null
+++ b/client/state/selectors/get-media-item.js
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import { get } from 'lodash';
+
+/**
+ * Returns a media object by site ID, media ID, or null if not known
+ *
+ * @param {Object} state Global state tree
+ * @param {Number} siteId Site ID
+ * @param {Number} mediaId Media ID
+ * @return {?Object} Media object, if known
+ */
+export default function getMediaItem( state, siteId, mediaId ) {
+ return get( state.media.items, [ siteId, mediaId ], null );
+}
diff --git a/client/state/selectors/get-media-url.js b/client/state/selectors/get-media-url.js
new file mode 100644
index 0000000000000..a4d27cc1f111f
--- /dev/null
+++ b/client/state/selectors/get-media-url.js
@@ -0,0 +1,22 @@
+/**
+ * Internal dependencies
+ */
+import safeImageUrl from 'lib/safe-image-url';
+import { getMediaItem } from './';
+
+/**
+ * Returns the URL for a media item, or null if not known
+ *
+ * @param {Object} state Global state tree
+ * @param {Number} siteId Site ID
+ * @param {Number} mediaId Media ID
+ * @return {?String} Media URL, if known
+ */
+export default function getMediaUrl( state, siteId, mediaId ) {
+ const media = getMediaItem( state, siteId, mediaId );
+ if ( ! media ) {
+ return null;
+ }
+
+ return safeImageUrl( media.URL );
+}
diff --git a/client/state/selectors/get-site-icon-id.js b/client/state/selectors/get-site-icon-id.js
new file mode 100644
index 0000000000000..466fc58960e47
--- /dev/null
+++ b/client/state/selectors/get-site-icon-id.js
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { get } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { getRawSite } from 'state/sites/selectors';
+import { getSiteSettings } from 'state/site-settings/selectors';
+
+/**
+ * Returns a ID to the media associated with a site's current site icon, or
+ * null if not known or an icon is not assigned.
+ *
+ * @param {Object} state Global state tree
+ * @param {Number} siteId Site ID
+ * @return {?Number} Media ID of site icon, if known and exists
+ */
+export default function getSiteIconId( state, siteId ) {
+ // Treat site object as preferred source of truth of media ID
+ const site = getRawSite( state, siteId );
+ if ( site ) {
+ return get( site, 'icon.media_id', null );
+ }
+
+ // Fall back to site settings in case we know settings prior to having
+ // received the site itself
+ const settings = getSiteSettings( state, siteId );
+ if ( settings ) {
+ return settings.site_icon;
+ }
+
+ return null;
+}
diff --git a/client/state/selectors/get-site-icon-url.js b/client/state/selectors/get-site-icon-url.js
new file mode 100644
index 0000000000000..e0a62cf759312
--- /dev/null
+++ b/client/state/selectors/get-site-icon-url.js
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+import { get } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { getRawSite } from 'state/sites/selectors';
+import { getSiteIconId, getMediaUrl } from './';
+
+/**
+ * Returns a URL to the site's current site icon, or null if no icon exists or
+ * if site is not known
+ *
+ * @param {Object} state Global state tree
+ * @param {Number} siteId Site ID
+ * @return {?String} URL of site icon, if known and exists
+ */
+export default function getSiteIconUrl( state, siteId ) {
+ const iconId = getSiteIconId( state, siteId );
+ const url = getMediaUrl( state, siteId, iconId );
+ if ( url ) {
+ return url;
+ }
+
+ // If cannot find media by ID, use icon.img property if available,
+ // otherwise assume icon is not set
+ return get( getRawSite( state, siteId ), 'icon.img', null );
+}
diff --git a/client/state/selectors/index.js b/client/state/selectors/index.js
index 02590a0736458..ca9c9bcf6f08d 100644
--- a/client/state/selectors/index.js
+++ b/client/state/selectors/index.js
@@ -14,3 +14,7 @@
*/
export canCurrentUser from './can-current-user';
+export getMediaItem from './get-media-item';
+export getMediaUrl from './get-media-url';
+export getSiteIconId from './get-site-icon-id';
+export getSiteIconUrl from './get-site-icon-url';
diff --git a/client/state/selectors/test/get-media-item.js b/client/state/selectors/test/get-media-item.js
new file mode 100644
index 0000000000000..bf3ec925b3049
--- /dev/null
+++ b/client/state/selectors/test/get-media-item.js
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+import { expect } from 'chai';
+
+/**
+ * Internal dependencies
+ */
+import { getMediaItem } from '../';
+
+describe( 'getMediaItem()', () => {
+ it( 'should return null if the site is not in state', () => {
+ const item = getMediaItem( {
+ media: {
+ items: {}
+ }
+ }, 2916284, 42 );
+
+ expect( item ).to.be.null;
+ } );
+
+ it( 'should return null if the media for the site is not in state', () => {
+ const item = getMediaItem( {
+ media: {
+ items: {
+ 2916284: {}
+ }
+ }
+ }, 2916284, 42 );
+
+ expect( item ).to.be.null;
+ } );
+
+ it( 'should return the media item', () => {
+ const item = getMediaItem( {
+ media: {
+ items: {
+ 2916284: {
+ 42: { ID: 42, title: 'flowers' }
+ }
+ }
+ }
+ }, 2916284, 42 );
+
+ expect( item ).to.eql( { ID: 42, title: 'flowers' } );
+ } );
+} );
diff --git a/client/state/selectors/test/get-media-url.js b/client/state/selectors/test/get-media-url.js
new file mode 100644
index 0000000000000..389deba2a9b5a
--- /dev/null
+++ b/client/state/selectors/test/get-media-url.js
@@ -0,0 +1,55 @@
+/**
+ * External dependencies
+ */
+import { expect } from 'chai';
+
+/**
+ * Internal dependencies
+ */
+import { getMediaUrl } from '../';
+
+describe( 'getMediaUrl()', () => {
+ it( 'should return null if the item is not in state', () => {
+ const url = getMediaUrl( {
+ media: {
+ items: {
+ 2916284: {}
+ }
+ }
+ }, 2916284, 42 );
+
+ expect( url ).to.be.null;
+ } );
+
+ it( 'should return null if the media item URL is invalid', () => {
+ const url = getMediaUrl( {
+ media: {
+ items: {
+ 2916284: {
+ 42: { ID: 42, title: 'flowers' }
+ }
+ }
+ }
+ }, 2916284, 42 );
+
+ expect( url ).to.be.null;
+ } );
+
+ it( 'should return a safe variation of the media URL', () => {
+ const url = getMediaUrl( {
+ media: {
+ items: {
+ 2916284: {
+ 42: {
+ ID: 42,
+ title: 'flowers',
+ URL: 'https://example.files.wordpress.com/2014/06/flower.gif'
+ }
+ }
+ }
+ }
+ }, 2916284, 42 );
+
+ expect( url ).to.equal( 'https://example.files.wordpress.com/2014/06/flower.gif' );
+ } );
+} );
diff --git a/client/state/selectors/test/get-site-icon-id.js b/client/state/selectors/test/get-site-icon-id.js
new file mode 100644
index 0000000000000..4fd3f185466da
--- /dev/null
+++ b/client/state/selectors/test/get-site-icon-id.js
@@ -0,0 +1,88 @@
+/**
+ * External dependencies
+ */
+import { expect } from 'chai';
+
+/**
+ * Internal dependencies
+ */
+import { getSiteIconId } from '../';
+
+describe( 'getSiteIconId()', () => {
+ it( 'should return null if neither the site nor settings are known', () => {
+ const id = getSiteIconId( {
+ sites: {
+ items: {}
+ },
+ siteSettings: {
+ items: {}
+ }
+ }, 2916284 );
+
+ expect( id ).to.be.null;
+ } );
+
+ it( 'should prefer site state', () => {
+ const id = getSiteIconId( {
+ sites: {
+ items: {
+ 2916284: {
+ ID: 2916284,
+ name: 'WordPress.com Example Blog',
+ icon: {
+ media_id: 42
+ }
+ }
+ }
+ },
+ siteSettings: {
+ items: {
+ 2916284: {
+ site_icon: 36
+ }
+ }
+ }
+ }, 2916284 );
+
+ expect( id ).to.equal( 42 );
+ } );
+
+ it( 'should prefer site state, even if unset', () => {
+ const id = getSiteIconId( {
+ sites: {
+ items: {
+ 2916284: {
+ ID: 2916284,
+ name: 'WordPress.com Example Blog'
+ }
+ }
+ },
+ siteSettings: {
+ items: {
+ 2916284: {
+ site_icon: 42
+ }
+ }
+ }
+ }, 2916284 );
+
+ expect( id ).to.be.null;
+ } );
+
+ it( 'should fall back to settings state', () => {
+ const id = getSiteIconId( {
+ sites: {
+ items: {}
+ },
+ siteSettings: {
+ items: {
+ 2916284: {
+ site_icon: 42
+ }
+ }
+ }
+ }, 2916284 );
+
+ expect( id ).to.equal( 42 );
+ } );
+} );
diff --git a/client/state/selectors/test/get-site-icon-url.js b/client/state/selectors/test/get-site-icon-url.js
new file mode 100644
index 0000000000000..0393fd26b56e6
--- /dev/null
+++ b/client/state/selectors/test/get-site-icon-url.js
@@ -0,0 +1,78 @@
+/**
+ * External dependencies
+ */
+import { expect } from 'chai';
+
+/**
+ * Internal dependencies
+ */
+import { getSiteIconUrl } from '../';
+
+describe( 'getSiteIconUrl()', () => {
+ it( 'should return null if neither the site nor site settings are known', () => {
+ const iconUrl = getSiteIconUrl( {
+ sites: {
+ items: {}
+ },
+ siteSettings: {
+ items: {}
+ },
+ media: {
+ items: {}
+ }
+ }, 2916284 );
+
+ expect( iconUrl ).to.be.null;
+ } );
+
+ it( 'should the site icon image as a fallback if the media is not known for the assigned icon ID', () => {
+ const iconUrl = getSiteIconUrl( {
+ sites: {
+ items: {
+ 2916284: {
+ ID: 2916284,
+ name: 'WordPress.com Example Blog',
+ icon: {
+ img: 'https://secure.gravatar.com/blavatar/0d6c430459af115394a012d20b6711d6',
+ ico: 'https://secure.gravatar.com/blavatar/0d6c430459af115394a012d20b6711d6'
+ }
+ }
+ }
+ },
+ media: {
+ items: {}
+ }
+ }, 2916284 );
+
+ expect( iconUrl ).to.equal( 'https://secure.gravatar.com/blavatar/0d6c430459af115394a012d20b6711d6' );
+ } );
+
+ it( 'should return the media URL via the site icon media ID', () => {
+ const iconUrl = getSiteIconUrl( {
+ sites: {
+ items: {
+ 2916284: {
+ ID: 2916284,
+ name: 'WordPress.com Example Blog',
+ icon: {
+ media_id: 42
+ }
+ }
+ }
+ },
+ media: {
+ items: {
+ 2916284: {
+ 42: {
+ ID: 42,
+ title: 'flowers',
+ URL: 'https://example.files.wordpress.com/2014/06/flower.gif'
+ }
+ }
+ }
+ }
+ }, 2916284 );
+
+ expect( iconUrl ).to.equal( 'https://example.files.wordpress.com/2014/06/flower.gif' );
+ } );
+} );