port d88a79b456
to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
master
parent
9c88792f0a
commit
8f950e540b
@ -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, |
||||
}); |
@ -0,0 +1,69 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import Icon from 'flavours/glitch/components/icon'; |
||||
import { removePictureInPicture } from 'flavours/glitch/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> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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 ( |
||||
<div className='picture-in-picture__footer'> |
||||
<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} counter={status.get('reblogs_count')} /> |
||||
<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} counter={status.get('favourites_count')} /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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 ( |
||||
<div className='picture-in-picture__header'> |
||||
<Link to={`/statuses/${statusId}`} className='picture-in-picture__header__account'> |
||||
<Avatar account={account} size={36} /> |
||||
<DisplayName account={account} /> |
||||
</Link> |
||||
|
||||
<IconButton icon='times' onClick={onClose} title='Close' /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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 = ( |
||||
<Video |
||||
src={src} |
||||
currentTime={this.props.currentTime} |
||||
volume={this.props.volume} |
||||
muted={this.props.muted} |
||||
autoPlay |
||||
inline |
||||
alwaysVisible |
||||
/> |
||||
); |
||||
} else if (type === 'audio') { |
||||
player = ( |
||||
<Audio |
||||
src={src} |
||||
currentTime={this.props.currentTime} |
||||
volume={this.props.volume} |
||||
muted={this.props.muted} |
||||
poster={this.props.poster} |
||||
backgroundColor={this.props.backgroundColor} |
||||
foregroundColor={this.props.foregroundColor} |
||||
accentColor={this.props.accentColor} |
||||
autoPlay |
||||
/> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className='picture-in-picture'> |
||||
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} /> |
||||
|
||||
{player} |
||||
|
||||
<Footer statusId={statusId} /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,22 @@ |
||||
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'flavours/glitch/actions/picture_in_picture'; |
||||
|
||||
const initialState = { |
||||
statusId: null, |
||||
accountId: null, |
||||
type: null, |
||||
src: null, |
||||
muted: false, |
||||
volume: 0, |
||||
currentTime: 0, |
||||
}; |
||||
|
||||
export default function pictureInPicture(state = initialState, action) { |
||||
switch(action.type) { |
||||
case PICTURE_IN_PICTURE_DEPLOY: |
||||
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props }; |
||||
case PICTURE_IN_PICTURE_REMOVE: |
||||
return { ...initialState }; |
||||
default: |
||||
return state; |
||||
} |
||||
}; |
Loading…
Reference in new issue