diff --git a/package.json b/package.json index 89ecba684d9cc9a1ca86d7d979606f4bc1c3e2a1..7283a9b905cebe188e22287e285b731596f0c4a3 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,6 @@ "react-test-renderer": "16.2.0", "react-tooltip": "3.8.0", "react-virtualized": "9.20.1", - "react-visibility-sensor": "5.0.2", "redux": "3.7.2", "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", @@ -110,7 +109,7 @@ "scratch-l10n": "3.1.20190410143750", "scratch-paint": "0.2.0-prerelease.20190318170811", "scratch-render": "0.1.0-prerelease.20190326161919", - "scratch-storage": "1.3.1", + "scratch-storage": "1.2.2", "scratch-svg-renderer": "0.2.0-prerelease.20190329052730", "scratch-vm": "0.2.0-prerelease.20190410143927", "selenium-webdriver": "3.6.0", diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 3ae872e8ad795f2d3b24d1eb4d6b2bec60e5a210..5af6fb15956cac258263186d819a0d2bba3e375e 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; -import ScratchImage from '../scratch-image/scratch-image.jsx'; import styles from './library-item.css'; import classNames from 'classnames'; @@ -36,12 +35,10 @@ class LibraryItemComponent extends React.PureComponent { /> </div> ) : null} - {this.props.iconSource ? ( - <ScratchImage - className={styles.featuredImage} - imageSource={this.props.iconSource} - /> - ) : null} + <img + className={styles.featuredImage} + src={this.props.iconURL} + /> </div> {this.props.insetIconURL ? ( <div className={styles.libraryItemInsetImageContainer}> @@ -124,9 +121,9 @@ class LibraryItemComponent extends React.PureComponent { {/* Layers of wrapping is to prevent layout thrashing on animation */} <Box className={styles.libraryItemImageContainerWrapper}> <Box className={styles.libraryItemImageContainer}> - <ScratchImage + <img className={styles.libraryItemImage} - imageSource={this.props.iconSource} + src={this.props.iconURL} /> </Box> </Box> @@ -149,7 +146,7 @@ LibraryItemComponent.propTypes = { extensionId: PropTypes.string, featured: PropTypes.bool, hidden: PropTypes.bool, - iconSource: ScratchImage.ImageSourcePropType, + iconURL: PropTypes.string, insetIconURL: PropTypes.string, internetConnectionRequired: PropTypes.bool, name: PropTypes.oneOfType([ diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 89e820f1b4452abd64b56834d7ebada2c21c3352..60f65a06d5760df690a9bbea8788b426e1629a23 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -9,7 +9,6 @@ import Modal from '../../containers/modal.jsx'; import Divider from '../divider/divider.jsx'; import Filter from '../filter/filter.jsx'; import TagButton from '../../containers/tag-button.jsx'; -import storage from '../../lib/storage'; import styles from './library.css'; @@ -29,47 +28,6 @@ const messages = defineMessages({ const ALL_TAG = {tag: 'all', intlLabel: messages.allTag}; const tagListPrefix = [ALL_TAG]; -/** - * Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap. - * @param {string} fileExtension - the file extension to look up. - * @returns {AssetType} - the AssetType corresponding to the extension, if any. - */ -const getAssetTypeForFileExtension = function (fileExtension) { - const compareOptions = { - sensitivity: 'accent', - usage: 'search' - }; - for (const assetTypeId in storage.AssetType) { - const assetType = storage.AssetType[assetTypeId]; - if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) { - return assetType; - } - } -}; - -/** - * Figure out an `imageSource` (URI or asset ID & type) for a library item's icon. - * @param {object} item - either a library item or one of a library item's costumes. - * @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`. - */ -const getItemImageSource = function (item) { - if (item.rawURL) { - return { - uri: item.rawURL - }; - } - - // TODO: adjust libraries to be more storage-friendly; don't use split() here. - const md5 = item.md5 || item.baseLayerMD5; - if (md5) { - const [assetId, fileExtension] = md5.split('.'); - return { - assetId: assetId, - assetType: getAssetTypeForFileExtension(fileExtension) - }; - } -}; - class LibraryComponent extends React.Component { constructor (props) { super(props); @@ -204,10 +162,8 @@ class LibraryComponent extends React.Component { })} ref={this.setFilteredDataRef} > - {this.getFilteredData().map((dataItem, index) => { - const iconSource = getItemImageSource(dataItem); - const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource); - return (<LibraryItem + {this.getFilteredData().map((dataItem, index) => ( + <LibraryItem bluetoothRequired={dataItem.bluetoothRequired} collaborator={dataItem.collaborator} description={dataItem.description} @@ -215,8 +171,9 @@ class LibraryComponent extends React.Component { extensionId={dataItem.extensionId} featured={dataItem.featured} hidden={dataItem.hidden} - iconSource={iconSource} - icons={icons} + iconMd5={dataItem.md5} + iconRawURL={dataItem.rawURL} + icons={dataItem.json && dataItem.json.costumes} id={index} insetIconURL={dataItem.insetIconURL} internetConnectionRequired={dataItem.internetConnectionRequired} @@ -225,8 +182,8 @@ class LibraryComponent extends React.Component { onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onSelect={this.handleSelect} - />); - })} + /> + ))} </div> </Modal> ); diff --git a/src/components/scratch-image/scratch-image.jsx b/src/components/scratch-image/scratch-image.jsx deleted file mode 100644 index 15abe9d3fca411ef8012f9865ffc0ab9fdf343f3..0000000000000000000000000000000000000000 --- a/src/components/scratch-image/scratch-image.jsx +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import VisibilitySensor from 'react-visibility-sensor'; - -import storage from '../../lib/storage'; - -class ScratchImage extends React.PureComponent { - static init () { - this._maxParallelism = 6; - this._currentJobs = 0; - this._pendingImages = new Set(); - } - - static loadPendingImages () { - if (this._currentJobs >= this._maxParallelism) { - // already busy - return; - } - - // Find the first visible image. If there aren't any, find the first non-visible image. - let nextImage; - for (const image of this._pendingImages) { - if (image.isVisible) { - nextImage = image; - break; - } else { - nextImage = nextImage || image; - } - } - - // If we found an image to load: - // 1) Remove it from the queue - // 2) Load the image - // 3) Pump the queue again - if (nextImage) { - this._pendingImages.delete(nextImage); - const imageSource = nextImage.props.imageSource; - ++this._currentJobs; - storage - .load(imageSource.assetType, imageSource.assetId) - .then(asset => { - if (!nextImage.wasUnmounted) { - const dataURI = asset.encodeDataURI(); - - nextImage.setState({ - imageURI: dataURI - }); - } - --this._currentJobs; - this.loadPendingImages(); - }); - } - } - - constructor (props) { - super(props); - this.state = {}; - Object.assign(this.state, this._loadImageSource(props.imageSource)); - } - componentWillReceiveProps (nextProps) { - const newState = this._loadImageSource(nextProps.imageSource); - this.setState(newState); - } - componentWillUnmount () { - this.wasUnmounted = true; - ScratchImage._pendingImages.delete(this); - } - /** - * Calculate the state changes necessary to load the image specified in the provided source info. If the component - * is mounted, call setState() with the return value of this function. If the component has not yet mounted, use - * the return value of this function as initial state for the component. - * - * @param {object} imageSource - the new source for the image, including either assetId or URI - * @returns {object} - the new state values, if any. - */ - _loadImageSource (imageSource) { - if (imageSource) { - if (imageSource.uri) { - ScratchImage._pendingImages.delete(this); - return { - imageURI: imageSource.uri, - lastRequestedAsset: null - }; - } - if (this.state.lastRequestedAsset !== imageSource.assetId) { - ScratchImage._pendingImages.add(this); - return { - lastRequestedAsset: imageSource.assetId - }; - } - } - // Nothing to do - don't change any state. - return {}; - } - render () { - const { - src: _src, - imageSource: _imageSource, - ...imgProps - } = this.props; - return ( - <VisibilitySensor - intervalCheck - scrollCheck - > - { - ({isVisible}) => { - this.isVisible = isVisible; - ScratchImage.loadPendingImages(); - return ( - <img - src={this.state.imageURI} - {...imgProps} - /> - ); - } - } - </VisibilitySensor> - ); - } -} - -ScratchImage.ImageSourcePropType = PropTypes.oneOfType([ - PropTypes.shape({ - assetId: PropTypes.string.isRequired, - assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired - }), - PropTypes.shape({ - uri: PropTypes.string.isRequired - }) -]); - -ScratchImage.propTypes = { - imageSource: ScratchImage.ImageSourcePropType.isRequired -}; - -ScratchImage.init(); - -export default ScratchImage; diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx index 0f770546d301904115e42cc27430f5f00c91ae89..6177b916fd69c00107decb03b1d0de13073194ae 100644 --- a/src/containers/library-item.jsx +++ b/src/containers/library-item.jsx @@ -75,17 +75,21 @@ class LibraryItem extends React.PureComponent { const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length; this.setState({iconIndex: nextIconIndex}); } - curIconSource () { + curIconMd5 () { if (this.props.icons && this.state.isRotatingIcon && this.state.iconIndex < this.props.icons.length && - this.props.icons[this.state.iconIndex]) { - return this.props.icons[this.state.iconIndex]; + this.props.icons[this.state.iconIndex] && + this.props.icons[this.state.iconIndex].baseLayerMD5) { + return this.props.icons[this.state.iconIndex].baseLayerMD5; } - return this.props.iconSource; + return this.props.iconMd5; } render () { - const iconSource = this.curIconSource(); + const iconMd5 = this.curIconMd5(); + const iconURL = iconMd5 ? + `https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` : + this.props.iconRawURL; return ( <LibraryItemComponent bluetoothRequired={this.props.bluetoothRequired} @@ -95,7 +99,8 @@ class LibraryItem extends React.PureComponent { extensionId={this.props.extensionId} featured={this.props.featured} hidden={this.props.hidden} - iconSource={iconSource} + iconURL={iconURL} + icons={this.props.icons} id={this.props.id} insetIconURL={this.props.insetIconURL} internetConnectionRequired={this.props.internetConnectionRequired} @@ -122,8 +127,13 @@ LibraryItem.propTypes = { extensionId: PropTypes.string, featured: PropTypes.bool, hidden: PropTypes.bool, - iconSource: LibraryItemComponent.propTypes.iconSource, // single icon - icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons + iconMd5: PropTypes.string, + iconRawURL: PropTypes.string, + icons: PropTypes.arrayOf( + PropTypes.shape({ + baseLayerMD5: PropTypes.string + }) + ), id: PropTypes.number.isRequired, insetIconURL: PropTypes.string, internetConnectionRequired: PropTypes.bool, diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index 6b6a021dda9e48acc3932f84e01417b017b0d363..0d1a784f6f7fdbdc259ad1fa561e3bd0f8d7c297 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -193,11 +193,7 @@ describe('Working with costumes', () => { .mouseMove(abbyElement) .perform(); // wait for one of Abby's alternate costumes to appear - const src1 = await abbyElement.findElement({css: 'img'}).getAttribute('src'); - await driver.sleep(300); - const src2 = await abbyElement.findElement({css: 'img'}).getAttribute('src'); - const sourcesMatch = (src1 === src2); - await expect(sourcesMatch).toBeFalsy(); // 'src' attribute should have changed by now + await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]'); const logs = await getLogs(); await expect(logs).toEqual([]); });