Skip to content

Commit

Permalink
Add pop-out player for audio/video in web UI (#14870)
Browse files Browse the repository at this point in the history
Fix #11160
  • Loading branch information
Gargron committed Sep 28, 2020
1 parent 5bbc9a4 commit d88a79b
Show file tree
Hide file tree
Showing 20 changed files with 648 additions and 58 deletions.
38 changes: 38 additions & 0 deletions app/javascript/mastodon/actions/picture_in_picture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @ts-check

export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';

/**
* @typedef MediaProps
* @property {string} src
* @property {boolean} muted
* @property {number} volume
* @property {number} currentTime
* @property {string} poster
* @property {string} backgroundColor
* @property {string} foregroundColor
* @property {string} accentColor
*/

/**
* @param {string} statusId
* @param {string} accountId
* @param {string} playerType
* @param {MediaProps} props
* @return {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});

/*
* @return {object}
*/
export const removePictureInPicture = () => ({
type: PICTURE_IN_PICTURE_REMOVE,
});
17 changes: 14 additions & 3 deletions app/javascript/mastodon/components/animated_number.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { reduceMotion } from 'mastodon/initial_state';

const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};

export default class AnimatedNumber extends React.PureComponent {

static propTypes = {
value: PropTypes.number.isRequired,
obfuscate: PropTypes.bool,
};

state = {
Expand Down Expand Up @@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent {
}

render () {
const { value } = this.props;
const { value, obfuscate } = this.props;
const { direction } = this.state;

if (reduceMotion) {
return <FormattedNumber value={value} />;
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
}

const styles = [{
Expand All @@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
{items => (
<span className='animated-number'>
{items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
))}
</span>
)}
Expand Down
11 changes: 10 additions & 1 deletion app/javascript/mastodon/components/icon_button.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';

export default class IconButton extends React.PureComponent {

Expand All @@ -24,6 +25,8 @@ export default class IconButton extends React.PureComponent {
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
counter: PropTypes.number,
obfuscateCount: PropTypes.bool,
};

static defaultProps = {
Expand Down Expand Up @@ -97,6 +100,8 @@ export default class IconButton extends React.PureComponent {
pressed,
tabIndex,
title,
counter,
obfuscateCount,
} = this.props;

const {
Expand All @@ -113,6 +118,10 @@ export default class IconButton extends React.PureComponent {
overlayed: overlay,
});

if (typeof counter !== 'undefined') {
style.width = 'auto';
}

return (
<button
aria-label={title}
Expand All @@ -128,7 +137,7 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
</button>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { FormattedMessage } from 'react-intl';

export default @connect()
class PictureInPicturePlaceholder extends React.PureComponent {

static propTypes = {
width: PropTypes.number,
dispatch: PropTypes.func.isRequired,
};

state = {
width: this.props.width,
height: this.props.width && (this.props.width / (16/9)),
};

handleClick = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
}

setRef = c => {
this.node = c;

if (this.node) {
this._setDimensions();
}
}

_setDimensions () {
const width = this.node.offsetWidth;
const height = width / (16/9);

this.setState({ width, height });
}

componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}

componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}

handleResize = debounce(() => {
if (this.node) {
this._setDimensions();
}
}, 250, {
trailing: true,
});

render () {
const { height } = this.state;

return (
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
<Icon id='window-restore' />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div>
);
}

}
19 changes: 17 additions & 2 deletions app/javascript/mastodon/components/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';

// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
Expand Down Expand Up @@ -95,6 +96,8 @@ class Status extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func,
usingPiP: PropTypes.bool,
};

// Avoid checking props that are functions (and whose equality will always
Expand All @@ -105,6 +108,7 @@ class Status extends ImmutablePureComponent {
'muted',
'hidden',
'unread',
'usingPiP',
];

state = {
Expand Down Expand Up @@ -206,6 +210,13 @@ class Status extends ImmutablePureComponent {
}
}

handleDeployPictureInPicture = (type, mediaProps) => {
const { deployPictureInPicture } = this.props;
const status = this._properStatus();

deployPictureInPicture(status, type, mediaProps);
}

handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history);
Expand Down Expand Up @@ -266,7 +277,7 @@ class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend, rebloggedByText;

const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props;
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, usingPiP } = this.props;

let { status, account, ...other } = this.props;

Expand Down Expand Up @@ -337,7 +348,9 @@ class Status extends ImmutablePureComponent {
status = status.get('reblog');
}

if (status.get('media_attachments').size > 0) {
if (usingPiP) {
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
} else if (status.get('media_attachments').size > 0) {
if (this.props.muted) {
media = (
<AttachmentList
Expand All @@ -362,6 +375,7 @@ class Status extends ImmutablePureComponent {
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={this.handleDeployPictureInPicture}
/>
)}
</Bundle>
Expand All @@ -383,6 +397,7 @@ class Status extends ImmutablePureComponent {
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={this.handleDeployPictureInPicture}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
Expand Down
13 changes: 2 additions & 11 deletions app/javascript/mastodon/components/status_action_bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,6 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});

const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};

const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
Expand Down Expand Up @@ -329,9 +319,10 @@ class StatusActionBar extends ImmutablePureComponent {

return (
<div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />

{shareButton}

<div className='status__action-bar-dropdown'>
Expand Down
6 changes: 6 additions & 0 deletions app/javascript/mastodon/containers/status_container.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { deployPictureInPicture } from '../actions/picture_in_picture';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
import { showAlertForError } from '../actions/alerts';
Expand All @@ -56,6 +57,7 @@ const makeMapStateToProps = () => {

const mapStateToProps = (state, props) => ({
status: getStatus(state, props),
usingPiP: state.get('picture_in_picture').statusId === props.id,
});

return mapStateToProps;
Expand Down Expand Up @@ -207,6 +209,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockDomain(domain));
},

deployPictureInPicture (status, type, mediaProps) {
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
},

});

export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
Loading

1 comment on commit d88a79b

@pullopen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to make the pop-out player movable? It will sometimes hide the "writing toots" button on mobile or people's replies on PC...

Please sign in to comment.