diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js deleted file mode 100644 index 898375abe..000000000 --- a/app/javascript/mastodon/actions/picture_in_picture.js +++ /dev/null @@ -1,46 +0,0 @@ -// @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 - * @returns {object} - */ -export const deployPictureInPicture = (statusId, accountId, playerType, props) => { - // @ts-expect-error - return (dispatch, getState) => { - // Do not open a player for a toot that does not exist - if (getState().hasIn(['statuses', statusId])) { - dispatch({ - type: PICTURE_IN_PICTURE_DEPLOY, - statusId, - accountId, - playerType, - props, - }); - } - }; -}; - -/* - * @return {object} - */ -export const removePictureInPicture = () => ({ - type: PICTURE_IN_PICTURE_REMOVE, -}); diff --git a/app/javascript/mastodon/actions/picture_in_picture.ts b/app/javascript/mastodon/actions/picture_in_picture.ts new file mode 100644 index 000000000..d34b508a3 --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.ts @@ -0,0 +1,31 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture'; +import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; + +interface DeployParams { + statusId: string; + accountId: string; + playerType: 'audio' | 'video'; + props: PIPMediaProps; +} + +export const removePictureInPicture = createAction('pip/remove'); + +export const deployPictureInPictureAction = + createAction('pip/deploy'); + +export const deployPictureInPicture = createAppAsyncThunk( + 'pip/deploy', + (args: DeployParams, { dispatch, getState }) => { + const { statusId } = args; + + // Do not open a player for a toot that does not exist + + // @ts-expect-error state.statuses is not yet typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (getState().hasIn(['statuses', statusId])) { + dispatch(deployPictureInPictureAction(args)); + } + }, +); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 7bd91c8c9..da93a16b0 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -262,7 +262,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, deployPictureInPicture (status, type, mediaProps) { - dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps})); }, onInteractionModal (type, status) { diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index 8dfbf54cb..a7d8356be 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -210,4 +210,4 @@ class Footer extends ImmutablePureComponent { } -export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer))); +export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer))); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx b/app/javascript/mastodon/features/picture_in_picture/components/header.jsx deleted file mode 100644 index 31073d738..000000000 --- a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { Avatar } from 'mastodon/components/avatar'; -import { DisplayName } from 'mastodon/components/display_name'; -import { IconButton } from 'mastodon/components/icon_button'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -const mapStateToProps = (state, { accountId }) => ({ - account: state.getIn(['accounts', accountId]), -}); - -class Header extends ImmutablePureComponent { - - static propTypes = { - accountId: PropTypes.string.isRequired, - statusId: PropTypes.string.isRequired, - account: ImmutablePropTypes.record.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { account, statusId, onClose, intl } = this.props; - - return ( -
- - - - - - -
- ); - } - -} - -export default connect(mapStateToProps)(injectIntl(Header)); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.tsx b/app/javascript/mastodon/features/picture_in_picture/components/header.tsx new file mode 100644 index 000000000..0f897dc44 --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.tsx @@ -0,0 +1,46 @@ +import { defineMessages, useIntl } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { Avatar } from 'mastodon/components/avatar'; +import { DisplayName } from 'mastodon/components/display_name'; +import { IconButton } from 'mastodon/components/icon_button'; +import { useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +interface Props { + accountId: string; + statusId: string; + onClose: () => void; +} + +export const Header: React.FC = ({ accountId, statusId, onClose }) => { + const account = useAppSelector((state) => state.accounts.get(accountId)); + + const intl = useIntl(); + + if (!account) return null; + + return ( +
+ + + + + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/picture_in_picture/index.jsx b/app/javascript/mastodon/features/picture_in_picture/index.jsx deleted file mode 100644 index f087cd1b1..000000000 --- a/app/javascript/mastodon/features/picture_in_picture/index.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import { Component } from 'react'; - -import { connect } from 'react-redux'; - -import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; -import Audio from 'mastodon/features/audio'; -import Video from 'mastodon/features/video'; - -import Footer from './components/footer'; -import Header from './components/header'; - -const mapStateToProps = state => ({ - ...state.get('picture_in_picture'), -}); - -class PictureInPicture extends 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 = ( -