[Glitch] Rewrite PIP state in Typescript
Port 9fbe8d3a0c
to glitch-soc
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
059e10e546
commit
371c5e59eb
13 changed files with 229 additions and 222 deletions
|
@ -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,
|
||||
});
|
31
app/javascript/flavours/glitch/actions/picture_in_picture.ts
Normal file
31
app/javascript/flavours/glitch/actions/picture_in_picture.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { PIPMediaProps } from 'flavours/glitch/reducers/picture_in_picture';
|
||||
import { createAppAsyncThunk } from 'flavours/glitch/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));
|
||||
}
|
||||
},
|
||||
);
|
|
@ -282,7 +282,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
deployPictureInPicture (status, type, mediaProps) {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['local_settings', 'media', 'pop_in_player'])) {
|
||||
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}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -237,4 +237,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 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/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 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { useAppSelector } from 'flavours/glitch/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,93 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
import Audio from 'flavours/glitch/features/audio';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
|
||||
import Footer from './components/footer';
|
||||
import Header from './components/header';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...state.get('picture_in_picture'),
|
||||
left: state.getIn(['local_settings', 'media', 'pop_in_position']) === 'left',
|
||||
});
|
||||
|
||||
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,
|
||||
left: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(removePictureInPicture());
|
||||
};
|
||||
|
||||
render () {
|
||||
const { type, src, currentTime, accountId, statusId, left } = 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={classNames('picture-in-picture', { left })}>
|
||||
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
|
||||
|
||||
{player}
|
||||
|
||||
<Footer statusId={statusId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PictureInPicture);
|
|
@ -0,0 +1,90 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
import Audio from 'flavours/glitch/features/audio';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from 'flavours/glitch/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);
|
||||
|
||||
const left = useAppSelector(
|
||||
// @ts-expect-error - `local_settings` is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
(s) => s.getIn(['local_settings', 'media', 'pop_in_position']) === 'left',
|
||||
);
|
||||
|
||||
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={classNames('picture-in-picture', { left })}>
|
||||
<Header accountId={accountId} statusId={statusId} onClose={handleClose} />
|
||||
|
||||
{player}
|
||||
|
||||
<Footer statusId={statusId} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -16,7 +16,7 @@ import { changeLayout } from 'flavours/glitch/actions/app';
|
|||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||
import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import PictureInPicture from 'flavours/glitch/features/picture_in_picture';
|
||||
import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture';
|
||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import { modalReducer } from './modal';
|
|||
import { notificationPolicyReducer } from './notification_policy';
|
||||
import { notificationRequestsReducer } from './notification_requests';
|
||||
import notifications from './notifications';
|
||||
import picture_in_picture from './picture_in_picture';
|
||||
import { pictureInPictureReducer } from './picture_in_picture';
|
||||
import pinnedAccountsEditor from './pinned_accounts_editor';
|
||||
import polls from './polls';
|
||||
import push_notifications from './push_notifications';
|
||||
|
@ -82,7 +82,7 @@ const reducers = {
|
|||
polls,
|
||||
trends,
|
||||
markers: markersReducer,
|
||||
picture_in_picture,
|
||||
picture_in_picture: pictureInPictureReducer,
|
||||
history,
|
||||
tags,
|
||||
followed_tags,
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'flavours/glitch/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 'flavours/glitch/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;
|
||||
};
|
|
@ -61,7 +61,7 @@ export const makeGetStatus = () => {
|
|||
|
||||
export const makeGetPictureInPicture = () => {
|
||||
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',
|
||||
], (inUse, available) => ImmutableMap({
|
||||
inUse: inUse && available,
|
||||
|
|
Loading…
Reference in a new issue