Rewrite PIP state in Typescript (#27645)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>main-rebase-security-fix
parent
b016f03637
commit
9fbe8d3a0c
|
@ -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,
|
|
||||||
});
|
|
|
@ -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<DeployParams>('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));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -262,7 +262,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
deployPictureInPicture (status, type, mediaProps) {
|
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) {
|
onInteractionModal (type, status) {
|
||||||
|
|
|
@ -210,4 +210,4 @@ class Footer extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));
|
export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer)));
|
||||||
|
|
|
@ -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 (
|
|
||||||
<div className='picture-in-picture__header'>
|
|
||||||
<Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
|
|
||||||
<Avatar account={account} size={36} />
|
|
||||||
<DisplayName account={account} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<IconButton icon='times' iconComponent={CloseIcon} onClick={onClose} title={intl.formatMessage(messages.close)} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(injectIntl(Header));
|
|
|
@ -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<Props> = ({ accountId, statusId, onClose }) => {
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='picture-in-picture__header'>
|
||||||
|
<Link
|
||||||
|
to={`/@${account.get('acct')}/${statusId}`}
|
||||||
|
className='picture-in-picture__header__account'
|
||||||
|
>
|
||||||
|
<Avatar account={account} size={36} />
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={onClose}
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 = (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(PictureInPicture);
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
|
||||||
|
import Audio from 'mastodon/features/audio';
|
||||||
|
import Video from 'mastodon/features/video';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
|
import Footer from './components/footer';
|
||||||
|
import { Header } from './components/header';
|
||||||
|
|
||||||
|
export const PictureInPicture: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
dispatch(removePictureInPicture());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const pipState = useAppSelector((s) => s.picture_in_picture);
|
||||||
|
|
||||||
|
if (pipState.type === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
src,
|
||||||
|
currentTime,
|
||||||
|
accountId,
|
||||||
|
statusId,
|
||||||
|
volume,
|
||||||
|
muted,
|
||||||
|
poster,
|
||||||
|
backgroundColor,
|
||||||
|
foregroundColor,
|
||||||
|
accentColor,
|
||||||
|
} = pipState;
|
||||||
|
|
||||||
|
let player;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'video':
|
||||||
|
player = (
|
||||||
|
<Video
|
||||||
|
src={src}
|
||||||
|
currentTime={currentTime}
|
||||||
|
volume={volume}
|
||||||
|
muted={muted}
|
||||||
|
autoPlay
|
||||||
|
inline
|
||||||
|
alwaysVisible
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'audio':
|
||||||
|
player = (
|
||||||
|
<Audio
|
||||||
|
src={src}
|
||||||
|
currentTime={currentTime}
|
||||||
|
volume={volume}
|
||||||
|
muted={muted}
|
||||||
|
poster={poster}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
foregroundColor={foregroundColor}
|
||||||
|
accentColor={accentColor}
|
||||||
|
autoPlay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='picture-in-picture'>
|
||||||
|
<Header accountId={accountId} statusId={statusId} onClose={handleClose} />
|
||||||
|
|
||||||
|
{player}
|
||||||
|
|
||||||
|
<Footer statusId={statusId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ import { HotKeys } from 'react-hotkeys';
|
||||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||||
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||||
import PictureInPicture from 'mastodon/features/picture_in_picture';
|
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
|
||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { modalReducer } from './modal';
|
||||||
import { notificationPolicyReducer } from './notification_policy';
|
import { notificationPolicyReducer } from './notification_policy';
|
||||||
import { notificationRequestsReducer } from './notification_requests';
|
import { notificationRequestsReducer } from './notification_requests';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
import picture_in_picture from './picture_in_picture';
|
import { pictureInPictureReducer } from './picture_in_picture';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
import { relationshipsReducer } from './relationships';
|
import { relationshipsReducer } from './relationships';
|
||||||
|
@ -78,7 +78,7 @@ const reducers = {
|
||||||
polls,
|
polls,
|
||||||
trends,
|
trends,
|
||||||
markers: markersReducer,
|
markers: markersReducer,
|
||||||
picture_in_picture,
|
picture_in_picture: pictureInPictureReducer,
|
||||||
history,
|
history,
|
||||||
tags,
|
tags,
|
||||||
followed_tags,
|
followed_tags,
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
|
|
||||||
|
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
|
||||||
|
|
||||||
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 };
|
|
||||||
case TIMELINE_DELETE:
|
|
||||||
return (state.statusId === action.id) ? { ...initialState } : state;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { Reducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deployPictureInPictureAction,
|
||||||
|
removePictureInPicture,
|
||||||
|
} from 'mastodon/actions/picture_in_picture';
|
||||||
|
|
||||||
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
|
|
||||||
|
export interface PIPMediaProps {
|
||||||
|
src: string;
|
||||||
|
muted: boolean;
|
||||||
|
volume: number;
|
||||||
|
currentTime: number;
|
||||||
|
poster: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
foregroundColor: string;
|
||||||
|
accentColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PIPStateWithValue extends Partial<PIPMediaProps> {
|
||||||
|
statusId: string;
|
||||||
|
accountId: string;
|
||||||
|
type: 'audio' | 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PIPStateEmpty extends Partial<PIPMediaProps> {
|
||||||
|
type: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PIPState = PIPStateWithValue | PIPStateEmpty;
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
type: null,
|
||||||
|
muted: false,
|
||||||
|
volume: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pictureInPictureReducer: Reducer<PIPState> = (
|
||||||
|
state = initialState,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
if (deployPictureInPictureAction.match(action))
|
||||||
|
return {
|
||||||
|
statusId: action.payload.statusId,
|
||||||
|
accountId: action.payload.accountId,
|
||||||
|
type: action.payload.playerType,
|
||||||
|
...action.payload.props,
|
||||||
|
};
|
||||||
|
else if (removePictureInPicture.match(action)) return initialState;
|
||||||
|
else if (action.type === TIMELINE_DELETE)
|
||||||
|
if (state.type && state.statusId === action.id) return initialState;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
|
@ -60,7 +60,7 @@ export const makeGetStatus = () => {
|
||||||
|
|
||||||
export const makeGetPictureInPicture = () => {
|
export const makeGetPictureInPicture = () => {
|
||||||
return createSelector([
|
return createSelector([
|
||||||
(state, { id }) => state.get('picture_in_picture').statusId === id,
|
(state, { id }) => state.picture_in_picture.statusId === id,
|
||||||
(state) => state.getIn(['meta', 'layout']) !== 'mobile',
|
(state) => state.getIn(['meta', 'layout']) !== 'mobile',
|
||||||
], (inUse, available) => ImmutableMap({
|
], (inUse, available) => ImmutableMap({
|
||||||
inUse: inUse && available,
|
inUse: inUse && available,
|
||||||
|
|
Loading…
Reference in New Issue