diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx')
-rw-r--r-- | browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx new file mode 100644 index 0000000000..8a6cefed3a --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx @@ -0,0 +1,263 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +const PLACEHOLDER_IMAGE_DATA_ARRAY = [ + { + rotation: "0deg", + offsetx: "20px", + offsety: "8px", + scale: "45%", + }, + { + rotation: "54deg", + offsetx: "-26px", + offsety: "62px", + scale: "55%", + }, + { + rotation: "-30deg", + offsetx: "78px", + offsety: "30px", + scale: "68%", + }, + { + rotation: "-22deg", + offsetx: "0", + offsety: "92px", + scale: "60%", + }, + { + rotation: "-65deg", + offsetx: "66px", + offsety: "28px", + scale: "60%", + }, + { + rotation: "22deg", + offsetx: "-35px", + offsety: "62px", + scale: "52%", + }, + { + rotation: "-25deg", + offsetx: "86px", + offsety: "-15px", + scale: "68%", + }, +]; + +const PLACEHOLDER_IMAGE_COLORS_ARRAY = + "#0090ED #FF4F5F #2AC3A2 #FF7139 #A172FF #FFA437 #FF2A8A".split(" "); + +function generateIndex({ keyCode, max }) { + if (!keyCode) { + // Just grab a random index if we cannot generate an index from a key. + return Math.floor(Math.random() * max); + } + + const hashStr = str => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + let charCode = str.charCodeAt(i); + hash += charCode; + } + return hash; + }; + + const hash = hashStr(keyCode); + return hash % max; +} + +export function PlaceholderImage({ urlKey, titleKey }) { + const dataIndex = generateIndex({ + keyCode: urlKey, + max: PLACEHOLDER_IMAGE_DATA_ARRAY.length, + }); + const colorIndex = generateIndex({ + keyCode: titleKey, + max: PLACEHOLDER_IMAGE_COLORS_ARRAY.length, + }); + const { rotation, offsetx, offsety, scale } = + PLACEHOLDER_IMAGE_DATA_ARRAY[dataIndex]; + const color = PLACEHOLDER_IMAGE_COLORS_ARRAY[colorIndex]; + const style = { + "--placeholderBackgroundColor": color, + "--placeholderBackgroundRotation": rotation, + "--placeholderBackgroundOffsetx": offsetx, + "--placeholderBackgroundOffsety": offsety, + "--placeholderBackgroundScale": scale, + }; + + return <div style={style} className="placeholder-image" />; +} + +export class DSImage extends React.PureComponent { + constructor(props) { + super(props); + + this.onOptimizedImageError = this.onOptimizedImageError.bind(this); + this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this); + this.onLoad = this.onLoad.bind(this); + + this.state = { + isLoaded: false, + optimizedImageFailed: false, + useTransition: false, + }; + } + + onIdleCallback() { + if (!this.state.isLoaded) { + this.setState({ + useTransition: true, + }); + } + } + + reformatImageURL(url, width, height) { + // Change the image URL to request a size tailored for the parent container width + // Also: force JPEG, quality 60, no upscaling, no EXIF data + // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html + return `https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent( + url + )}`; + } + + componentDidMount() { + this.idleCallbackId = this.props.windowObj.requestIdleCallback( + this.onIdleCallback.bind(this) + ); + } + + componentWillUnmount() { + if (this.idleCallbackId) { + this.props.windowObj.cancelIdleCallback(this.idleCallbackId); + } + } + + render() { + let classNames = `ds-image + ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``} + ${this.state && this.state.useTransition ? ` use-transition` : ``} + ${this.state && this.state.isLoaded ? ` loaded` : ``} + `; + + let img; + + if (this.state) { + if ( + this.props.optimize && + this.props.rawSource && + !this.state.optimizedImageFailed + ) { + let baseSource = this.props.rawSource; + + let sizeRules = []; + let srcSetRules = []; + + for (let rule of this.props.sizes) { + let { mediaMatcher, width, height } = rule; + let sizeRule = `${mediaMatcher} ${width}px`; + sizeRules.push(sizeRule); + let srcSetRule = `${this.reformatImageURL( + baseSource, + width, + height + )} ${width}w`; + let srcSetRule2x = `${this.reformatImageURL( + baseSource, + width * 2, + height * 2 + )} ${width * 2}w`; + srcSetRules.push(srcSetRule); + srcSetRules.push(srcSetRule2x); + } + + if (this.props.sizes.length) { + // We have to supply a fallback in the very unlikely event that none of + // the media queries match. The smallest dimension was chosen arbitrarily. + sizeRules.push( + `${this.props.sizes[this.props.sizes.length - 1].width}px` + ); + } + + img = ( + <img + loading="lazy" + alt={this.props.alt_text} + crossOrigin="anonymous" + onLoad={this.onLoad} + onError={this.onOptimizedImageError} + sizes={sizeRules.join(",")} + src={baseSource} + srcSet={srcSetRules.join(",")} + /> + ); + } else if (this.props.source && !this.state.nonOptimizedImageFailed) { + img = ( + <img + loading="lazy" + alt={this.props.alt_text} + crossOrigin="anonymous" + onLoad={this.onLoad} + onError={this.onNonOptimizedImageError} + src={this.props.source} + /> + ); + } else { + // We consider a failed to load img or source without an image as loaded. + classNames = `${classNames} loaded`; + // Remove the img element if we have no source. Render a placeholder instead. + // This only happens for recent saves without a source. + if ( + this.props.isRecentSave && + !this.props.rawSource && + !this.props.source + ) { + img = ( + <PlaceholderImage + urlKey={this.props.url} + titleKey={this.props.title} + /> + ); + } else { + img = <div className="broken-image" />; + } + } + } + + return <picture className={classNames}>{img}</picture>; + } + + onOptimizedImageError() { + // This will trigger a re-render and the unoptimized 450px image will be used as a fallback + this.setState({ + optimizedImageFailed: true, + }); + } + + onNonOptimizedImageError() { + this.setState({ + nonOptimizedImageFailed: true, + }); + } + + onLoad() { + this.setState({ + isLoaded: true, + }); + } +} + +DSImage.defaultProps = { + source: null, // The current source style from Pocket API (always 450px) + rawSource: null, // Unadulterated image URL to filter through Thumbor + extraClassNames: null, // Additional classnames to append to component + optimize: true, // Measure parent container to request exact sizes + alt_text: null, + windowObj: window, // Added to support unit tests + sizes: [], +}; |