diff --git a/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap index c3139fe69..2938fb3ed 100644 --- a/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap @@ -2,30 +2,36 @@ exports[` Autoplay renders an animated avatar 1`] = `
+> + +
`; exports[` Still renders a still avatar 1`] = `
+> + +
`; diff --git a/app/soapbox/components/__tests__/__snapshots__/avatar_overlay-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/avatar_overlay-test.js.snap index d59fee42f..6481d1b69 100644 --- a/app/soapbox/components/__tests__/__snapshots__/avatar_overlay-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/avatar_overlay-test.js.snap @@ -5,20 +5,24 @@ exports[`
+ className="account__avatar-overlay-base still-image" + style={Object {}} + > + +
+ className="account__avatar-overlay-overlay still-image" + style={Object {}} + > + +
`; diff --git a/app/soapbox/components/__tests__/avatar-test.js b/app/soapbox/components/__tests__/avatar-test.js index f7f430c7a..297b0b413 100644 --- a/app/soapbox/components/__tests__/avatar-test.js +++ b/app/soapbox/components/__tests__/avatar-test.js @@ -1,7 +1,7 @@ import React from 'react'; -import renderer from 'react-test-renderer'; import { fromJS } from 'immutable'; -import { Avatar } from '../avatar'; +import { createComponent } from 'soapbox/test_helpers'; +import Avatar from '../avatar'; describe('', () => { const account = fromJS({ @@ -16,7 +16,7 @@ describe('', () => { describe('Autoplay', () => { it('renders an animated avatar', () => { - const component = renderer.create(); + const component = createComponent(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); @@ -25,7 +25,7 @@ describe('', () => { describe('Still', () => { it('renders a still avatar', () => { - const component = renderer.create(); + const component = createComponent(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); diff --git a/app/soapbox/components/__tests__/avatar_overlay-test.js b/app/soapbox/components/__tests__/avatar_overlay-test.js index ea75dab57..c469dcc75 100644 --- a/app/soapbox/components/__tests__/avatar_overlay-test.js +++ b/app/soapbox/components/__tests__/avatar_overlay-test.js @@ -1,7 +1,7 @@ import React from 'react'; -import renderer from 'react-test-renderer'; import { fromJS } from 'immutable'; -import { AvatarOverlay } from '../avatar_overlay'; +import { createComponent } from 'soapbox/test_helpers'; +import AvatarOverlay from '../avatar_overlay'; describe(' { const account = fromJS({ @@ -21,7 +21,7 @@ describe(' { }); it('renders a overlay avatar', () => { - const component = renderer.create(); + const component = createComponent(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); diff --git a/app/soapbox/components/avatar.js b/app/soapbox/components/avatar.js index a4358332b..e909c44df 100644 --- a/app/soapbox/components/avatar.js +++ b/app/soapbox/components/avatar.js @@ -1,54 +1,25 @@ import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { getSettings } from 'soapbox/actions/settings'; +import classNames from 'classnames'; +import StillImage from 'soapbox/components/still_image'; -const mapStateToProps = state => ({ - animate: getSettings(state).get('autoPlayGif'), -}); - -export class Avatar extends React.PureComponent { +export default class Avatar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map, size: PropTypes.number, style: PropTypes.object, inline: PropTypes.bool, - animate: PropTypes.bool, }; static defaultProps = { inline: false, }; - state = { - hovering: false, - }; - - handleMouseEnter = () => { - if (this.props.animate) return; - this.setState({ hovering: true }); - } - - handleMouseLeave = () => { - if (this.props.animate) return; - this.setState({ hovering: false }); - } - render() { - const { account, size, animate, inline } = this.props; + const { account, size, inline } = this.props; if (!account) return null; - const { hovering } = this.state; - - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - let className = 'account__avatar'; - - if (inline) { - className = className + ' account__avatar-inline'; - } // : TODO : remove inline and change all avatars to be sized using css const style = !size ? {} : { @@ -56,22 +27,14 @@ export class Avatar extends React.PureComponent { height: `${size}px`, }; - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - return ( -
); } } - -export default connect(mapStateToProps)(Avatar); diff --git a/app/soapbox/components/avatar_composite.js b/app/soapbox/components/avatar_composite.js index 06607cacd..c6d1eb8cc 100644 --- a/app/soapbox/components/avatar_composite.js +++ b/app/soapbox/components/avatar_composite.js @@ -1,24 +1,16 @@ import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { getSettings } from 'soapbox/actions/settings'; +import StillImage from 'soapbox/components/still_image'; -const mapStateToProps = state => ({ - animate: getSettings(state).get('autoPlayGif'), -}); - -export default @connect(mapStateToProps) -class AvatarComposite extends React.PureComponent { +export default class AvatarComposite extends React.PureComponent { static propTypes = { accounts: ImmutablePropTypes.list.isRequired, - animate: PropTypes.bool, size: PropTypes.number.isRequired, }; renderItem(account, size, index) { - const { animate } = this.props; let width = 50; let height = 100; @@ -76,12 +68,10 @@ class AvatarComposite extends React.PureComponent { bottom: bottom, width: `${width}%`, height: `${height}%`, - backgroundSize: 'cover', - backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`, }; return ( -
+ ); } diff --git a/app/soapbox/components/avatar_overlay.js b/app/soapbox/components/avatar_overlay.js index a72c4e614..802fe423a 100644 --- a/app/soapbox/components/avatar_overlay.js +++ b/app/soapbox/components/avatar_overlay.js @@ -1,40 +1,23 @@ import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { getSettings } from 'soapbox/actions/settings'; +import StillImage from 'soapbox/components/still_image'; -const mapStateToProps = state => ({ - animate: getSettings(state).get('autoPlayGif'), -}); - -export class AvatarOverlay extends React.PureComponent { +export default class AvatarOverlay extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, friend: ImmutablePropTypes.map.isRequired, - animate: PropTypes.bool, }; render() { - const { account, friend, animate } = this.props; - - const baseStyle = { - backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`, - }; - - const overlayStyle = { - backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`, - }; + const { account, friend } = this.props; return (
-
-
+ +
); } } - -export default connect(mapStateToProps)(AvatarOverlay); diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js index 4ab6e53e0..92886b715 100644 --- a/app/soapbox/components/media_gallery.js +++ b/app/soapbox/components/media_gallery.js @@ -11,6 +11,7 @@ import { decode } from 'blurhash'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'; import { Map as ImmutableMap } from 'immutable'; import { getSettings } from 'soapbox/actions/settings'; +import StillImage from 'soapbox/components/still_image'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, @@ -60,8 +61,7 @@ class Item extends React.PureComponent { hoverToPlay() { const { attachment, autoPlayGif } = this.props; - return !autoPlayGif && - (attachment.get('type') === 'gifv' || attachment.getIn(['pleroma', 'mime_type']) === 'image/gif'); + return !autoPlayGif && attachment.get('type') === 'gifv'; } handleClick = (e) => { @@ -72,7 +72,7 @@ class Item extends React.PureComponent { e.preventDefault(); } else { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - if (!this.canvas && this.hoverToPlay()) { + if (this.hoverToPlay()) { e.target.pause(); e.target.currentTime = 0; } @@ -112,23 +112,12 @@ class Item extends React.PureComponent { this.canvas = c; } - setImageRef = i => { - this.image = i; - } - handleImageLoad = () => { this.setState({ loaded: true }); - if (this.hoverToPlay()) { - const image = this.image; - const canvas = this.canvas; - canvas.width = image.naturalWidth; - canvas.height = image.naturalHeight; - canvas.getContext('2d').drawImage(image, 0, 0); - } } render() { - const { attachment, standalone, displayWidth, visible, dimensions, autoPlayGif } = this.props; + const { attachment, standalone, visible, dimensions, autoPlayGif } = this.props; let width = 100; let height = '100%'; @@ -162,39 +151,17 @@ class Item extends React.PureComponent { ); } else if (attachment.get('type') === 'image') { const previewUrl = attachment.get('preview_url'); - const previewWidth = attachment.getIn(['meta', 'small', 'width']); const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); - - const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; - - const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null; - const sizes = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null; - - const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; - const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; thumbnail = ( - {attachment.get('description')} - {this.hoverToPlay() && } + ); } else if (attachment.get('type') === 'gifv') { diff --git a/app/soapbox/components/still_image.js b/app/soapbox/components/still_image.js new file mode 100644 index 000000000..ef02b13ef --- /dev/null +++ b/app/soapbox/components/still_image.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { getSettings } from 'soapbox/actions/settings'; +import classNames from 'classnames'; + +const mapStateToProps = state => ({ + autoPlayGif: getSettings(state).get('autoPlayGif'), +}); + +export default @connect(mapStateToProps) +class StillImage extends React.PureComponent { + + static propTypes = { + alt: PropTypes.string, + autoPlayGif: PropTypes.bool.isRequired, + className: PropTypes.node, + src: PropTypes.string.isRequired, + style: PropTypes.object, + }; + + static defaultProps = { + alt: '', + className: '', + style: {}, + } + + hoverToPlay() { + const { autoPlayGif, src } = this.props; + return !autoPlayGif && (src.endsWith('.gif') || src.startsWith('blob:')); + } + + setCanvasRef = c => { + this.canvas = c; + } + + setImageRef = i => { + this.img = i; + } + + handleImageLoad = () => { + if (this.hoverToPlay()) { + const img = this.img; + const canvas = this.canvas; + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + canvas.getContext('2d').drawImage(img, 0, 0); + } + } + + render() { + const { alt, className, src, style } = this.props; + const hoverToPlay = this.hoverToPlay(); + + return ( +
+ {alt} + {hoverToPlay && } +
+ ); + } + +} diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 83c87e0bf..908c71a50 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -16,7 +16,7 @@ import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import ProfileInfoPanel from '../../ui/components/profile_info_panel'; import { debounce } from 'lodash'; -import { getSettings } from 'soapbox/actions/settings'; +import StillImage from 'soapbox/components/still_image'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -54,7 +54,6 @@ const mapStateToProps = state => { return { me, isStaff: isStaff(state.getIn(['accounts', me])), - autoPlayGif: getSettings(state).get('autoPlayGif'), version: parseVersion(state.getIn(['instance', 'version'])), }; }; @@ -70,7 +69,6 @@ class Header extends ImmutablePureComponent { onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, username: PropTypes.string, - autoPlayGif: PropTypes.bool, isStaff: PropTypes.bool.isRequired, version: PropTypes.object, }; @@ -226,7 +224,7 @@ class Header extends ImmutablePureComponent { }; render() { - const { account, intl, username, me, autoPlayGif } = this.props; + const { account, intl, username, me } = this.props; const { isSmallScreen } = this.state; if (!account) { @@ -252,8 +250,7 @@ class Header extends ImmutablePureComponent { const actionBtn = this.getActionBtn(); const menu = this.makeMenu(); - const headerImgSrc = autoPlayGif ? account.get('header') : account.get('header_static'); - const headerMissing = (headerImgSrc.indexOf('/headers/original/missing.png') > -1); + const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1); const avatarSize = isSmallScreen ? 90 : 200; @@ -264,7 +261,7 @@ class Header extends ImmutablePureComponent { {info}
- +
diff --git a/app/soapbox/features/account_gallery/components/media_item.js b/app/soapbox/features/account_gallery/components/media_item.js index 670e465a8..05f8867d4 100644 --- a/app/soapbox/features/account_gallery/components/media_item.js +++ b/app/soapbox/features/account_gallery/components/media_item.js @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { decode } from 'blurhash'; import { isIOS } from 'soapbox/is_mobile'; import { getSettings } from 'soapbox/actions/settings'; +import StillImage from 'soapbox/components/still_image'; const mapStateToProps = state => ({ autoPlayGif: getSettings(state).get('autoPlayGif'), @@ -113,12 +114,10 @@ class MediaItem extends ImmutablePureComponent { const y = ((focusY / -2) + .5) * 100; thumbnail = ( - {attachment.get('description')} ); } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { diff --git a/app/soapbox/features/edit_profile/components/profile_preview.js b/app/soapbox/features/edit_profile/components/profile_preview.js index 0e8923207..6b4166d22 100644 --- a/app/soapbox/features/edit_profile/components/profile_preview.js +++ b/app/soapbox/features/edit_profile/components/profile_preview.js @@ -1,16 +1,17 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { acctFull } from 'soapbox/utils/accounts'; +import StillImage from 'soapbox/components/still_image'; const ProfilePreview = ({ account }) => (
- +
- +
{account.get('username')} diff --git a/app/soapbox/features/ui/components/user_panel.js b/app/soapbox/features/ui/components/user_panel.js index d434ff4cc..83dfd2099 100644 --- a/app/soapbox/features/ui/components/user_panel.js +++ b/app/soapbox/features/ui/components/user_panel.js @@ -9,7 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Avatar from 'soapbox/components/avatar'; import { shortNumberFormat } from 'soapbox/utils/numbers'; import { acctFull } from 'soapbox/utils/accounts'; -import { getSettings } from 'soapbox/actions/settings'; +import StillImage from 'soapbox/components/still_image'; class UserPanel extends ImmutablePureComponent { @@ -20,7 +20,7 @@ class UserPanel extends ImmutablePureComponent { } render() { - const { account, intl, domain, autoPlayGif } = this.props; + const { account, intl, domain } = this.props; if (!account) return null; const displayNameHtml = { __html: account.get('display_name_html') }; const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); @@ -30,7 +30,7 @@ class UserPanel extends ImmutablePureComponent {
- +
@@ -91,7 +91,6 @@ const mapStateToProps = state => { return { account: getAccount(state, me), - autoPlayGif: getSettings(state).get('autoPlayGif'), }; }; diff --git a/app/styles/accounts.scss b/app/styles/accounts.scss index 02dec30f8..da3025186 100644 --- a/app/styles/accounts.scss +++ b/app/styles/accounts.scss @@ -24,7 +24,7 @@ background: var(--background-color); border-radius: 4px 4px 0 0; - img { + .still-image { display: block; width: 100%; height: 100%; @@ -61,7 +61,7 @@ height: 48px; padding-top: 2px; - img { + .still-image { width: 100%; height: 100%; display: block; diff --git a/app/styles/application.scss b/app/styles/application.scss index 331e38f92..62682fc9c 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -58,6 +58,7 @@ @import 'components/navigation-bar'; @import 'components/promo-panel'; @import 'components/drawer'; +@import 'components/still-image'; @import 'components/timeline-queue-header'; @import 'components/badge'; @import 'components/trends'; diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index 7fff5095d..78a9e7e78 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -21,7 +21,6 @@ background: var(--accent-color--med); @media screen and (max-width: 895px) {height: 225px;} &--none {height: 125px;} - img { object-fit: cover; display: block; @@ -29,6 +28,23 @@ height: 100%; margin: 0; } + + .still-image--play-on-hover::before { + content: 'GIF'; + position: absolute; + color: var(--primary-text-color); + background: var(--foreground-color); + top: 6px; + left: 6px; + padding: 2px 6px; + border-radius: 2px; + font-size: 11px; + font-weight: 600; + pointer-events: none; + opacity: 0.9; + transition: opacity 0.1s ease; + line-height: 18px; + } } &__bar { @@ -58,6 +74,23 @@ background-size: 200px 200px; } + .still-image--play-on-hover::before { + content: 'GIF'; + position: absolute; + color: var(--primary-text-color); + background: var(--foreground-color); + bottom: 15%; + left: 15%; + padding: 1px 4px; + border-radius: 2px; + font-size: 8px; + font-weight: 600; + pointer-events: none; + opacity: 0.9; + transition: opacity 0.1s ease; + line-height: 13px; + } + @media screen and (max-width: 895px) { top: -45px; left: 10px; diff --git a/app/styles/components/media-gallery.scss b/app/styles/components/media-gallery.scss index 05ba65a41..431610bb6 100644 --- a/app/styles/components/media-gallery.scss +++ b/app/styles/components/media-gallery.scss @@ -28,47 +28,26 @@ z-index: 1; &, - img, - canvas { + .still-image { height: 100%; width: 100%; } - img, - canvas { - object-fit: cover; - } - - &--play-on-hover { - &::before { - content: 'GIF'; - position: absolute; - color: var(--primary-text-color); - background: var(--foreground-color); - bottom: 6px; - left: 6px; - padding: 2px 6px; - border-radius: 2px; - font-size: 11px; - font-weight: 600; - pointer-events: none; - opacity: 0.9; - transition: opacity 0.1s ease; - line-height: 18px; - } - - img { - position: absolute; - } - - img, - &:hover::before { - visibility: hidden; - } - - &:hover img { - visibility: visible; - } + .still-image--play-on-hover::before { + content: 'GIF'; + position: absolute; + color: var(--primary-text-color); + background: var(--foreground-color); + bottom: 6px; + left: 6px; + padding: 2px 6px; + border-radius: 2px; + font-size: 11px; + font-weight: 600; + pointer-events: none; + opacity: 0.9; + transition: opacity 0.1s ease; + line-height: 18px; } } @@ -82,6 +61,23 @@ z-index: 0; background: var(--background-color); + .still-image--play-on-hover::before { + content: 'GIF'; + position: absolute; + color: var(--primary-text-color); + background: var(--foreground-color); + bottom: 6px; + left: 6px; + padding: 2px 6px; + border-radius: 2px; + font-size: 11px; + font-weight: 600; + pointer-events: none; + opacity: 0.9; + transition: opacity 0.1s ease; + line-height: 18px; + } + &--hidden { display: none; } diff --git a/app/styles/components/still-image.scss b/app/styles/components/still-image.scss new file mode 100644 index 000000000..0b86d6ada --- /dev/null +++ b/app/styles/components/still-image.scss @@ -0,0 +1,28 @@ +.still-image { + position: relative; + overflow: hidden; + + img, + canvas { + width: 100%; + height: 100%; + display: block; + object-fit: cover; + font-family: inherit; + } + + &--play-on-hover { + img { + position: absolute; + visibility: hidden; + } + + &:hover img { + visibility: visible; + } + + &:hover canvas { + visibility: hidden; + } + } +}