From 8f950e540b83e13748c0df9bc30afbb06ef26f3e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 28 Sep 2020 13:29:43 +0200 Subject: [Glitch] Add pop-out player for audio/video in web UI port d88a79b4566869ede24958fbff946e357bbb3cb9 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/features/audio/index.js | 60 ++++++++- .../picture_in_picture/components/footer.js | 137 +++++++++++++++++++++ .../picture_in_picture/components/header.js | 40 ++++++ .../glitch/features/picture_in_picture/index.js | 85 +++++++++++++ .../features/status/components/detailed_status.js | 7 +- .../flavours/glitch/features/status/index.js | 5 +- .../glitch/features/ui/components/media_modal.js | 2 +- .../glitch/features/ui/components/video_modal.js | 4 +- .../flavours/glitch/features/ui/index.js | 2 + .../flavours/glitch/features/video/index.js | 66 ++++++++-- 10 files changed, 389 insertions(+), 19 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js create mode 100644 app/javascript/flavours/glitch/features/picture_in_picture/components/header.js create mode 100644 app/javascript/flavours/glitch/features/picture_in_picture/index.js (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js index 7a2fb7fb6..6d09ac8d2 100644 --- a/app/javascript/flavours/glitch/features/audio/index.js +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -37,7 +37,11 @@ class Audio extends React.PureComponent { backgroundColor: PropTypes.string, foregroundColor: PropTypes.string, accentColor: PropTypes.string, + currentTime: PropTypes.number, autoPlay: PropTypes.bool, + volume: PropTypes.number, + muted: PropTypes.bool, + deployPictureInPicture: PropTypes.func, }; state = { @@ -64,6 +68,19 @@ class Audio extends React.PureComponent { } } + _pack() { + return { + src: this.props.src, + volume: this.audio.volume, + muted: this.audio.muted, + currentTime: this.audio.currentTime, + poster: this.props.poster, + backgroundColor: this.props.backgroundColor, + foregroundColor: this.props.foregroundColor, + accentColor: this.props.accentColor, + }; + } + _setDimensions () { const width = this.player.offsetWidth; const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9)); @@ -100,6 +117,7 @@ class Audio extends React.PureComponent { } componentDidMount () { + window.addEventListener('scroll', this.handleScroll); window.addEventListener('resize', this.handleResize, { passive: true }); } @@ -115,7 +133,12 @@ class Audio extends React.PureComponent { } componentWillUnmount () { + window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.handleResize); + + if (!this.state.paused && this.audio && this.props.deployPictureInPicture) { + this.props.deployPictureInPicture('audio', this._pack()); + } } togglePlay = () => { @@ -243,6 +266,25 @@ class Audio extends React.PureComponent { } }, 15); + handleScroll = throttle(() => { + if (!this.canvas || !this.audio) { + return; + } + + const { top, height } = this.canvas.getBoundingClientRect(); + const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0); + + if (!this.state.paused && !inView) { + this.audio.pause(); + + if (this.props.deployPictureInPicture) { + this.props.deployPictureInPicture('audio', this._pack()); + } + + this.setState({ paused: true }); + } + }, 150, { trailing: true }); + handleMouseEnter = () => { this.setState({ hovered: true }); } @@ -252,10 +294,22 @@ class Audio extends React.PureComponent { } handleLoadedData = () => { - const { autoPlay } = this.props; + const { autoPlay, currentTime, volume, muted } = this.props; + + if (currentTime) { + this.audio.currentTime = currentTime; + } + + if (volume !== undefined) { + this.audio.volume = volume; + } + + if (muted !== undefined) { + this.audio.muted = muted; + } if (autoPlay) { - this.audio.play(); + this.togglePlay(); } } @@ -341,7 +395,7 @@ class Audio extends React.PureComponent { render () { const { src, intl, alt, editable, autoPlay } = this.props; const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state; - const progress = (currentTime / duration) * 100; + const progress = Math.min((currentTime / duration) * 100, 100); return (
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js new file mode 100644 index 000000000..75bd39dea --- /dev/null +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js @@ -0,0 +1,137 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'flavours/glitch/components/icon_button'; +import classNames from 'classnames'; +import { me, boostModal } from 'flavours/glitch/util/initial_state'; +import { defineMessages, injectIntl } from 'react-intl'; +import { replyCompose } from 'flavours/glitch/actions/compose'; +import { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions'; +import { makeGetStatus } from 'flavours/glitch/selectors'; +import { openModal } from 'flavours/glitch/actions/modal'; + +const messages = defineMessages({ + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, + reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, + cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, + cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, +}); + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, { statusId }) => ({ + status: getStatus(state, { id: statusId }), + askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, + }); + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) +@injectIntl +class Footer extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + statusId: PropTypes.string.isRequired, + status: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + askReplyConfirmation: PropTypes.bool, + }; + + _performReply = () => { + const { dispatch, status } = this.props; + dispatch(replyCompose(status, this.context.router.history)); + }; + + handleReplyClick = () => { + const { dispatch, askReplyConfirmation, intl } = this.props; + + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + })); + } else { + this._performReply(); + } + }; + + handleFavouriteClick = () => { + const { dispatch, status } = this.props; + + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } + }; + + _performReblog = () => { + const { dispatch, status } = this.props; + dispatch(reblog(status)); + } + + handleReblogClick = e => { + const { dispatch, status } = this.props; + + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else if ((e && e.shiftKey) || !boostModal) { + this._performReblog(); + } else { + dispatch(openModal('BOOST', { status, onReblog: this._performReblog })); + } + }; + + render () { + const { status, intl } = this.props; + + const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; + + let replyIcon, replyTitle; + + if (status.get('in_reply_to_id', null) === null) { + replyIcon = 'reply'; + replyTitle = intl.formatMessage(messages.reply); + } else { + replyIcon = 'reply-all'; + replyTitle = intl.formatMessage(messages.replyAll); + } + + let reblogTitle = ''; + + if (status.get('reblogged')) { + reblogTitle = intl.formatMessage(messages.cancel_reblog_private); + } else if (publicStatus) { + reblogTitle = intl.formatMessage(messages.reblog); + } else if (reblogPrivate) { + reblogTitle = intl.formatMessage(messages.reblog_private); + } else { + reblogTitle = intl.formatMessage(messages.cannot_reblog); + } + + return ( +
+ + + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js new file mode 100644 index 000000000..24adcde25 --- /dev/null +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { Link } from 'react-router-dom'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; + +const mapStateToProps = (state, { accountId }) => ({ + account: state.getIn(['accounts', accountId]), +}); + +export default @connect(mapStateToProps) +class Header extends ImmutablePureComponent { + + static propTypes = { + accountId: PropTypes.string.isRequired, + statusId: PropTypes.string.isRequired, + account: ImmutablePropTypes.map.isRequired, + onClose: PropTypes.func.isRequired, + }; + + render () { + const { account, statusId, onClose } = this.props; + + return ( +
+ + + + + + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/index.js b/app/javascript/flavours/glitch/features/picture_in_picture/index.js new file mode 100644 index 000000000..200f2fc7f --- /dev/null +++ b/app/javascript/flavours/glitch/features/picture_in_picture/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Video from 'flavours/glitch/features/video'; +import Audio from 'flavours/glitch/features/audio'; +import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import Header from './components/header'; +import Footer from './components/footer'; + +const mapStateToProps = state => ({ + ...state.get('picture_in_picture'), +}); + +export default @connect(mapStateToProps) +class PictureInPicture extends React.Component { + + static propTypes = { + statusId: PropTypes.string, + accountId: PropTypes.string, + type: PropTypes.string, + src: PropTypes.string, + muted: PropTypes.bool, + volume: PropTypes.number, + currentTime: PropTypes.number, + poster: PropTypes.string, + backgroundColor: PropTypes.string, + foregroundColor: PropTypes.string, + accentColor: PropTypes.string, + dispatch: PropTypes.func.isRequired, + }; + + handleClose = () => { + const { dispatch } = this.props; + dispatch(removePictureInPicture()); + } + + render () { + const { type, src, currentTime, accountId, statusId } = this.props; + + if (!currentTime) { + return null; + } + + let player; + + if (type === 'video') { + player = ( +
- {(!onCloseVideo && !editable && !fullscreen) && } + {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && } {(!fullscreen && onOpenVideo) && } {onCloseVideo && } -- cgit From 5ee2b860f902871c1f38ae0e9b8f333b38c73bbe Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 26 Oct 2020 20:03:33 +0100 Subject: Respect glitch-soc's reply counter setting in the pop-in player --- .../picture_in_picture/components/footer.js | 29 ++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js index 75bd39dea..2ddba140e 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js @@ -30,6 +30,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { statusId }) => ({ status: getStatus(state, { id: statusId }), askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, + showReplyCount: state.getIn(['local_settings', 'show_reply_count']), }); return mapStateToProps; @@ -49,6 +50,7 @@ class Footer extends ImmutablePureComponent { intl: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, askReplyConfirmation: PropTypes.bool, + showReplyCount: PropTypes.bool, }; _performReply = () => { @@ -98,7 +100,7 @@ class Footer extends ImmutablePureComponent { }; render () { - const { status, intl } = this.props; + const { status, intl, showReplyCount } = this.props; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; @@ -125,9 +127,32 @@ class Footer extends ImmutablePureComponent { reblogTitle = intl.formatMessage(messages.cannot_reblog); } + let replyButton = null; + if (showReplyCount) { + replyButton = ( + + ); + } else { + replyButton = ( + + ); + } + return (
- + {replyButton}
-- cgit From ea5298ab9b4b989119218807281b68d19756cd7a Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 26 Oct 2020 20:11:35 +0100 Subject: Add setting to disable the pop-in player --- app/javascript/flavours/glitch/containers/status_container.js | 6 +++++- .../flavours/glitch/features/local_settings/page/index.js | 8 ++++++++ app/javascript/flavours/glitch/reducers/local_settings.js | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 48b0d8255..ac423c58d 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -248,7 +248,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, deployPictureInPicture (status, type, mediaProps) { - dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + dispatch((_, getState) => { + if (getState().getIn(['local_settings', 'media', 'pop_in_player'])) { + dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + } + }); }, }); diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index 0b3428027..bce45901e 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -420,6 +420,14 @@ class LocalSettingsPage extends React.PureComponent { > + + +
), ]; diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 3d94d665c..e4df22b9f 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -49,6 +49,7 @@ const initialState = ImmutableMap({ letterbox : true, fullwidth : true, reveal_behind_cw : false, + pop_in_player : true, }), notifications : ImmutableMap({ favicon_badge : false, -- cgit From 49ee69f75f24ef13f36cb177d5278fcc3a5f3d37 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 26 Oct 2020 20:45:25 +0100 Subject: Add local setting for pop-in player position --- .../glitch/features/local_settings/page/index.js | 17 ++++++++++++++++- .../glitch/features/picture_in_picture/index.js | 7 +++++-- .../flavours/glitch/reducers/local_settings.js | 1 + .../flavours/glitch/styles/components/status.scss | 5 +++++ 4 files changed, 27 insertions(+), 3 deletions(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index bce45901e..45d10d154 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -28,6 +28,8 @@ const messages = defineMessages({ rewrite_mentions_no: { id: 'settings.rewrite_mentions_no', defaultMessage: 'Do not rewrite mentions' }, rewrite_mentions_acct: { id: 'settings.rewrite_mentions_acct', defaultMessage: 'Rewrite with username and domain (when the account is remote)' }, rewrite_mentions_username: { id: 'settings.rewrite_mentions_username', defaultMessage: 'Rewrite with username' }, + pop_in_left: { id: 'settings.pop_in_left', defaultMessage: 'Left' }, + pop_in_right: { id: 'settings.pop_in_right', defaultMessage: 'Right' }, }); export default @injectIntl @@ -384,7 +386,7 @@ class LocalSettingsPage extends React.PureComponent { ), - ({ onChange, settings }) => ( + ({ intl, onChange, settings }) => (

+ + +
), ]; diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/index.js b/app/javascript/flavours/glitch/features/picture_in_picture/index.js index 200f2fc7f..3e6a20faa 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/index.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/index.js @@ -6,9 +6,11 @@ import Audio from 'flavours/glitch/features/audio'; import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; import Header from './components/header'; import Footer from './components/footer'; +import classNames from 'classnames'; const mapStateToProps = state => ({ ...state.get('picture_in_picture'), + left: state.getIn(['local_settings', 'media', 'pop_in_position']) === 'left', }); export default @connect(mapStateToProps) @@ -27,6 +29,7 @@ class PictureInPicture extends React.Component { foregroundColor: PropTypes.string, accentColor: PropTypes.string, dispatch: PropTypes.func.isRequired, + left: PropTypes.bool, }; handleClose = () => { @@ -35,7 +38,7 @@ class PictureInPicture extends React.Component { } render () { - const { type, src, currentTime, accountId, statusId } = this.props; + const { type, src, currentTime, accountId, statusId, left } = this.props; if (!currentTime) { return null; @@ -72,7 +75,7 @@ class PictureInPicture extends React.Component { } return ( -
+
{player} diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index e4df22b9f..c115cad6b 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -50,6 +50,7 @@ const initialState = ImmutableMap({ fullwidth : true, reveal_behind_cw : false, pop_in_player : true, + pop_in_position : 'right', }), notifications : ImmutableMap({ favicon_badge : false, diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 7ec377bad..554ea8cd5 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -1066,6 +1066,11 @@ a.status-card.compact:hover { right: 20px; width: 300px; + &.left { + right: unset; + left: 20px; + } + &__footer { border-radius: 0 0 4px 4px; background: lighten($ui-base-color, 4%); -- cgit