Fix audio and video items in account gallery in web UI (#14282)

* Fix audio and video items in account gallery in web UI

- Fix audio items not using thumbnails
- Fix video items not using custom thumbnails
- Fix video items autoplaying like GIFs

* Change audio and video items in account gallery to autoplay when opened in web UI

* Fix code style issue
This commit is contained in:
Eugen Rochko 2020-07-10 22:09:28 +02:00 committed by GitHub
parent 96e89d1ef4
commit 6cc5b822f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 91 additions and 64 deletions

View file

@ -63,55 +63,7 @@ export default class MediaItem extends ImmutablePureComponent {
const status = attachment.get('status'); const status = attachment.get('status');
const title = status.get('spoiler_text') || attachment.get('description'); const title = status.get('spoiler_text') || attachment.get('description');
let thumbnail = ''; let thumbnail, label, icon, content;
let icon;
if (attachment.get('type') === 'unknown') {
// Skip
} else if (attachment.get('type') === 'audio') {
thumbnail = (
<span className='account-gallery__item__icons'>
<Icon id='music' />
</span>
);
} else if (attachment.get('type') === 'image') {
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 = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
title={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
const autoPlay = !isIOS() && autoPlayGif;
const label = attachment.get('type') === 'video' ? <Icon id='play' /> : 'GIF';
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
loop
muted
/>
<span className='media-gallery__gifv__label'>{label}</span>
</div>
);
}
if (!visible) { if (!visible) {
icon = ( icon = (
@ -119,6 +71,60 @@ export default class MediaItem extends ImmutablePureComponent {
<Icon id='eye-slash' /> <Icon id='eye-slash' />
</span> </span>
); );
} else {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
onLoad={this.handleImageLoad}
/>
);
if (attachment.get('type') === 'audio') {
label = <Icon id='music' />;
} else {
label = <Icon id='play' />;
}
} else if (attachment.get('type') === 'image') {
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;
content = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (attachment.get('type') === 'gifv') {
content = (
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={!isIOS() && autoPlayGif}
loop
muted
/>
);
label = 'GIF';
}
thumbnail = (
<div className='media-gallery__gifv'>
{content}
<span className='media-gallery__gifv__label'>{label}</span>
</div>
);
} }
return ( return (
@ -126,13 +132,11 @@ export default class MediaItem extends ImmutablePureComponent {
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'> <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<Blurhash <Blurhash
hash={attachment.get('blurhash')} hash={attachment.get('blurhash')}
className={classNames('media-gallery__preview', { className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
'media-gallery__preview--hidden': visible && loaded,
})}
dummy={!useBlurhash} dummy={!useBlurhash}
/> />
{visible && thumbnail}
{!visible && icon} {visible ? thumbnail : icon}
</a> </a>
</div> </div>
); );

View file

@ -101,9 +101,9 @@ class AccountGallery extends ImmutablePureComponent {
handleOpenMedia = attachment => { handleOpenMedia = attachment => {
if (attachment.get('type') === 'video') { if (attachment.get('type') === 'video') {
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') { } else if (attachment.get('type') === 'audio') {
this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status') })); this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
} else { } else {
const media = attachment.getIn(['status', 'media_attachments']); const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id')); const index = media.findIndex(x => x.get('id') === attachment.get('id'));

View file

@ -37,6 +37,7 @@ class Audio extends React.PureComponent {
backgroundColor: PropTypes.string, backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string, foregroundColor: PropTypes.string,
accentColor: PropTypes.string, accentColor: PropTypes.string,
autoPlay: PropTypes.bool,
}; };
state = { state = {
@ -259,6 +260,14 @@ class Audio extends React.PureComponent {
this.setState({ hovered: false }); this.setState({ hovered: false });
} }
handleLoadedData = () => {
const { autoPlay } = this.props;
if (autoPlay) {
this.audio.play();
}
}
_initAudioContext () { _initAudioContext () {
const context = new AudioContext(); const context = new AudioContext();
const source = context.createMediaElementSource(this.audio); const source = context.createMediaElementSource(this.audio);
@ -336,7 +345,7 @@ class Audio extends React.PureComponent {
} }
render () { render () {
const { src, intl, alt, editable } = this.props; const { src, intl, alt, editable, autoPlay } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state; const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
const progress = (currentTime / duration) * 100; const progress = (currentTime / duration) * 100;
@ -345,10 +354,11 @@ class Audio extends React.PureComponent {
<audio <audio
src={src} src={src}
ref={this.setAudioRef} ref={this.setAudioRef}
preload='none' preload={autoPlay ? 'auto' : 'none'}
onPlay={this.handlePlay} onPlay={this.handlePlay}
onPause={this.handlePause} onPause={this.handlePause}
onProgress={this.handleProgress} onProgress={this.handleProgress}
onLoadedData={this.handleLoadedData}
crossOrigin='anonymous' crossOrigin='anonymous'
/> />

View file

@ -2,17 +2,27 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio'; import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { previewState } from './video_modal'; import { previewState } from './video_modal';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
export default class AudioModal extends ImmutablePureComponent { const mapStateToProps = (state, { status }) => ({
account: state.getIn(['accounts', status.get('account')]),
});
export default @connect(mapStateToProps)
class AudioModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
options: PropTypes.shape({
autoPlay: PropTypes.bool,
}),
account: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
@ -50,7 +60,8 @@ export default class AudioModal extends ImmutablePureComponent {
} }
render () { render () {
const { media, status } = this.props; const { media, status, account } = this.props;
const options = this.props.options || {};
return ( return (
<div className='modal-root__modal audio-modal'> <div className='modal-root__modal audio-modal'>
@ -60,10 +71,11 @@ export default class AudioModal extends ImmutablePureComponent {
alt={media.get('description')} alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
poster={media.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={media.get('preview_url') || account.get('avatar_static')}
backgroundColor={media.getIn(['meta', 'colors', 'background'])} backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])} foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])} accentColor={media.getIn(['meta', 'colors', 'accent'])}
autoPlay={options.autoPlay}
/> />
</div> </div>

View file

@ -154,12 +154,13 @@ export const makeGetNotification = () => {
export const getAccountGallery = createSelector([ export const getAccountGallery = createSelector([
(state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()), (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
state => state.get('statuses'), state => state.get('statuses'),
], (statusIds, statuses) => { (state, id) => state.getIn(['accounts', id]),
], (statusIds, statuses, account) => {
let medias = ImmutableList(); let medias = ImmutableList();
statusIds.forEach(statusId => { statusIds.forEach(statusId => {
const status = statuses.get(statusId); const status = statuses.get(statusId);
medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status))); medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status).set('account', account)));
}); });
return medias; return medias;