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' ); + } ); +} );