Unify audio playback under App component

This commit is contained in:
Fedor Indutny 2021-06-29 12:58:29 -07:00 committed by GitHub
parent 8b30fc17cd
commit 2cd4160422
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 290 additions and 80 deletions

View file

@ -175,7 +175,8 @@ const globalContents: Contents = {
export const GlobalAudioContext = React.createContext<Contents>(globalContents); export const GlobalAudioContext = React.createContext<Contents>(globalContents);
export type GlobalAudioProps = { export type GlobalAudioProps = {
conversationId: string; conversationId: string | undefined;
isPaused: boolean;
children?: React.ReactNode | React.ReactChildren; children?: React.ReactNode | React.ReactChildren;
}; };
@ -185,6 +186,7 @@ export type GlobalAudioProps = {
*/ */
export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({ export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({
conversationId, conversationId,
isPaused,
children, children,
}) => { }) => {
// When moving between conversations - stop audio // When moving between conversations - stop audio
@ -194,6 +196,13 @@ export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({
}; };
}, [conversationId]); }, [conversationId]);
// Pause when requested by parent
React.useEffect(() => {
if (isPaused) {
globalContents.audio.pause();
}
}, [isPaused]);
return ( return (
<GlobalAudioContext.Provider value={globalContents}> <GlobalAudioContext.Provider value={globalContents}>
{children} {children}

View file

@ -47,19 +47,22 @@ const renderEmojiPicker: Props['renderEmojiPicker'] = ({
); );
const MessageAudioContainer: React.FC<AudioAttachmentProps> = props => { const MessageAudioContainer: React.FC<AudioAttachmentProps> = props => {
const [activeAudioID, setActiveAudioID] = React.useState<string | undefined>( const [active, setActive] = React.useState<{
undefined id?: string;
); context?: string;
}>({});
const audio = React.useMemo(() => new Audio(), []); const audio = React.useMemo(() => new Audio(), []);
return ( return (
<MessageAudio <MessageAudio
{...props} {...props}
id="storybook" id="storybook"
renderingContext="storybook"
audio={audio} audio={audio}
computePeaks={computePeaks} computePeaks={computePeaks}
setActiveAudioID={setActiveAudioID} setActiveAudioID={(id, context) => setActive({ id, context })}
activeAudioID={activeAudioID} activeAudioID={active.id}
activeAudioContext={active.context}
/> />
); );
}; };
@ -101,6 +104,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
undefined, undefined,
i18n, i18n,
id: text('id', overrideProps.id || ''), id: text('id', overrideProps.id || ''),
renderingContext: 'storybook',
interactionMode: overrideProps.interactionMode || 'keyboard', interactionMode: overrideProps.interactionMode || 'keyboard',
isSticker: isBoolean(overrideProps.isSticker) isSticker: isBoolean(overrideProps.isSticker)
? overrideProps.isSticker ? overrideProps.isSticker

View file

@ -89,6 +89,7 @@ export type DirectionType = typeof Directions[number];
export type AudioAttachmentProps = { export type AudioAttachmentProps = {
id: string; id: string;
renderingContext: string;
i18n: LocalizerType; i18n: LocalizerType;
buttonRef: React.RefObject<HTMLButtonElement>; buttonRef: React.RefObject<HTMLButtonElement>;
direction: DirectionType; direction: DirectionType;
@ -103,6 +104,7 @@ export type AudioAttachmentProps = {
export type PropsData = { export type PropsData = {
id: string; id: string;
renderingContext: string;
contactNameColor?: ContactNameColorType; contactNameColor?: ContactNameColorType;
conversationColor: ConversationColorType; conversationColor: ConversationColorType;
customColor?: CustomColorType; customColor?: CustomColorType;
@ -751,6 +753,7 @@ export class Message extends React.Component<Props, State> {
direction, direction,
i18n, i18n,
id, id,
renderingContext,
kickOffAttachmentDownload, kickOffAttachmentDownload,
markAttachmentAsCorrupted, markAttachmentAsCorrupted,
quote, quote,
@ -849,6 +852,7 @@ export class Message extends React.Component<Props, State> {
i18n, i18n,
buttonRef: this.audioButtonRef, buttonRef: this.audioButtonRef,
id, id,
renderingContext,
direction, direction,
theme, theme,
attachment: firstAttachment, attachment: firstAttachment,

View file

@ -14,6 +14,7 @@ import { ComputePeaksResult } from '../GlobalAudioContext';
export type Props = { export type Props = {
direction?: 'incoming' | 'outgoing'; direction?: 'incoming' | 'outgoing';
id: string; id: string;
renderingContext: string;
i18n: LocalizerType; i18n: LocalizerType;
attachment: AttachmentType; attachment: AttachmentType;
withContentAbove: boolean; withContentAbove: boolean;
@ -28,7 +29,8 @@ export type Props = {
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>; computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
activeAudioID: string | undefined; activeAudioID: string | undefined;
setActiveAudioID: (id: string | undefined) => void; activeAudioContext: string | undefined;
setActiveAudioID: (id: string | undefined, context: string) => void;
}; };
type ButtonProps = { type ButtonProps = {
@ -121,14 +123,19 @@ const Button: React.FC<ButtonProps> = props => {
* toggle Play/Pause button. * toggle Play/Pause button.
* *
* A global audio player is used for playback and access is managed by the * A global audio player is used for playback and access is managed by the
* `activeAudioID` property. Whenever `activeAudioID` property is equal to `id` * `activeAudioID` and `activeAudioContext` properties. Whenever both
* the instance of the `MessageAudio` assumes the ownership of the `Audio` * `activeAudioID` and `activeAudioContext` are equal to `id` and `context`
* instance and fully manages it. * respectively the instance of the `MessageAudio` assumes the ownership of the
* `Audio` instance and fully manages it.
*
* `context` is required for displaying separate MessageAudio instances in
* MessageDetails and Message React components.
*/ */
export const MessageAudio: React.FC<Props> = (props: Props) => { export const MessageAudio: React.FC<Props> = (props: Props) => {
const { const {
i18n, i18n,
id, id,
renderingContext,
direction, direction,
attachment, attachment,
withContentAbove, withContentAbove,
@ -142,12 +149,14 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
computePeaks, computePeaks,
activeAudioID, activeAudioID,
activeAudioContext,
setActiveAudioID, setActiveAudioID,
} = props; } = props;
assert(audio !== null, 'GlobalAudioContext always provides audio'); assert(audio !== null, 'GlobalAudioContext always provides audio');
const isActive = activeAudioID === id; const isActive =
activeAudioID === id && activeAudioContext === renderingContext;
const waveformRef = useRef<HTMLDivElement | null>(null); const waveformRef = useRef<HTMLDivElement | null>(null);
const [isPlaying, setIsPlaying] = useState(isActive && !audio.paused); const [isPlaying, setIsPlaying] = useState(isActive && !audio.paused);
@ -317,7 +326,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
if (!isActive && !isPlaying) { if (!isActive && !isPlaying) {
window.log.info('MessageAudio: changing owner', id); window.log.info('MessageAudio: changing owner', id);
setActiveAudioID(id); setActiveAudioID(id, renderingContext);
// Pause old audio // Pause old audio
if (!audio.paused) { if (!audio.paused) {

View file

@ -30,6 +30,7 @@ const defaultMessage: MessageDataPropsType = {
conversationType: 'direct', conversationType: 'direct',
direction: 'incoming', direction: 'incoming',
id: 'my-message', id: 'my-message',
renderingContext: 'storybook',
isBlocked: false, isBlocked: false,
isMessageRequestAccepted: true, isMessageRequestAccepted: true,
previews: [], previews: [],

View file

@ -6,7 +6,6 @@ import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { GlobalAudioProvider } from '../GlobalAudioContext';
import { Avatar } from '../Avatar'; import { Avatar } from '../Avatar';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { import {
@ -46,7 +45,7 @@ export type Props = {
contacts: Array<Contact>; contacts: Array<Contact>;
contactNameColor?: ContactNameColorType; contactNameColor?: ContactNameColorType;
errors: Array<Error>; errors: Array<Error>;
message: MessagePropsDataType; message: Omit<MessagePropsDataType, 'renderingContext'>;
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
@ -266,57 +265,54 @@ export class MessageDetail extends React.Component<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}> <div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
<div className="module-message-detail__message-container"> <div className="module-message-detail__message-container">
<GlobalAudioProvider conversationId={message.conversationId}> <Message
<Message {...message}
{...message} renderingContext="conversation/MessageDetail"
checkForAccount={checkForAccount} checkForAccount={checkForAccount}
clearSelectedMessage={clearSelectedMessage} clearSelectedMessage={clearSelectedMessage}
contactNameColor={contactNameColor} contactNameColor={contactNameColor}
deleteMessage={deleteMessage} deleteMessage={deleteMessage}
deleteMessageForEveryone={deleteMessageForEveryone} deleteMessageForEveryone={deleteMessageForEveryone}
disableMenu disableMenu
disableScroll disableScroll
displayTapToViewMessage={displayTapToViewMessage} displayTapToViewMessage={displayTapToViewMessage}
downloadAttachment={downloadAttachment} downloadAttachment={downloadAttachment}
doubleCheckMissingQuoteReference={ doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
doubleCheckMissingQuoteReference i18n={i18n}
} interactionMode={interactionMode}
i18n={i18n} kickOffAttachmentDownload={kickOffAttachmentDownload}
interactionMode={interactionMode} markAttachmentAsCorrupted={markAttachmentAsCorrupted}
kickOffAttachmentDownload={kickOffAttachmentDownload} onHeightChange={noop}
markAttachmentAsCorrupted={markAttachmentAsCorrupted} openConversation={openConversation}
onHeightChange={noop} openLink={openLink}
openConversation={openConversation} reactToMessage={reactToMessage}
openLink={openLink} renderAudioAttachment={renderAudioAttachment}
reactToMessage={reactToMessage} renderEmojiPicker={renderEmojiPicker}
renderAudioAttachment={renderAudioAttachment} replyToMessage={replyToMessage}
renderEmojiPicker={renderEmojiPicker} retrySend={retrySend}
replyToMessage={replyToMessage} showForwardMessageModal={showForwardMessageModal}
retrySend={retrySend} scrollToQuotedMessage={() => {
showForwardMessageModal={showForwardMessageModal} assert(
scrollToQuotedMessage={() => { false,
assert( 'scrollToQuotedMessage should never be called because scrolling is disabled'
false, );
'scrollToQuotedMessage should never be called because scrolling is disabled' }}
); showContactDetail={showContactDetail}
}} showContactModal={showContactModal}
showContactDetail={showContactDetail} showExpiredIncomingTapToViewToast={
showContactModal={showContactModal} showExpiredIncomingTapToViewToast
showExpiredIncomingTapToViewToast={ }
showExpiredIncomingTapToViewToast showExpiredOutgoingTapToViewToast={
} showExpiredOutgoingTapToViewToast
showExpiredOutgoingTapToViewToast={ }
showExpiredOutgoingTapToViewToast showMessageDetail={() => {
} assert(
showMessageDetail={() => { false,
assert( "showMessageDetail should never be called because the menu is disabled (and we're already in the message detail!)"
false, );
"showMessageDetail should never be called because the menu is disabled (and we're already in the message detail!)" }}
); showVisualAttachment={showVisualAttachment}
}} />
showVisualAttachment={showVisualAttachment}
/>
</GlobalAudioProvider>
</div> </div>
<table className="module-message-detail__info"> <table className="module-message-detail__info">
<tbody> <tbody>

View file

@ -50,6 +50,7 @@ const defaultMessageProps: MessagesProps = {
), ),
i18n, i18n,
id: 'messageId', id: 'messageId',
renderingContext: 'storybook',
interactionMode: 'keyboard', interactionMode: 'keyboard',
isBlocked: false, isBlocked: false,
isMessageRequestAccepted: true, isMessageRequestAccepted: true,

View file

@ -15,8 +15,6 @@ import Measure from 'react-measure';
import { ScrollDownButton } from './ScrollDownButton'; import { ScrollDownButton } from './ScrollDownButton';
import { GlobalAudioProvider } from '../GlobalAudioContext';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations'; import { ConversationType } from '../../state/ducks/conversations';
import { assert } from '../../util/assert'; import { assert } from '../../util/assert';
@ -1424,9 +1422,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
> >
{timelineWarning} {timelineWarning}
<GlobalAudioProvider conversationId={id}> {autoSizer}
{autoSizer}
</GlobalAudioProvider>
{shouldShowScrollDownButton ? ( {shouldShowScrollDownButton ? (
<ScrollDownButton <ScrollDownButton
conversationId={id} conversationId={id}

View file

@ -80,7 +80,7 @@ type LinkNotificationType = {
}; };
type MessageType = { type MessageType = {
type: 'message'; type: 'message';
data: MessageProps; data: Omit<MessageProps, 'renderingContext'>;
}; };
type UnsupportedMessageType = { type UnsupportedMessageType = {
type: 'unsupportedMessage'; type: 'unsupportedMessage';
@ -189,7 +189,13 @@ export class TimelineItem extends React.PureComponent<PropsType> {
if (item.type === 'message') { if (item.type === 'message') {
return ( return (
<Message {...this.props} {...item.data} i18n={i18n} theme={theme} /> <Message
{...this.props}
{...item.data}
i18n={i18n}
theme={theme}
renderingContext="conversation/TimelineItem"
/>
); );
} }

View file

@ -3,12 +3,17 @@
import { useBoundActions } from '../../util/hooks'; import { useBoundActions } from '../../util/hooks';
import { SwitchToAssociatedViewActionType } from './conversations'; import {
SwitchToAssociatedViewActionType,
MessageDeletedActionType,
MessageChangedActionType,
} from './conversations';
// State // State
export type AudioPlayerStateType = { export type AudioPlayerStateType = {
readonly activeAudioID: string | undefined; readonly activeAudioID: string | undefined;
readonly activeAudioContext: string | undefined;
}; };
// Actions // Actions
@ -17,6 +22,7 @@ type SetActiveAudioIDAction = {
type: 'audioPlayer/SET_ACTIVE_AUDIO_ID'; type: 'audioPlayer/SET_ACTIVE_AUDIO_ID';
payload: { payload: {
id: string | undefined; id: string | undefined;
context: string | undefined;
}; };
}; };
@ -30,10 +36,13 @@ export const actions = {
export const useActions = (): typeof actions => useBoundActions(actions); export const useActions = (): typeof actions => useBoundActions(actions);
function setActiveAudioID(id: string | undefined): SetActiveAudioIDAction { function setActiveAudioID(
id: string | undefined,
context: string
): SetActiveAudioIDAction {
return { return {
type: 'audioPlayer/SET_ACTIVE_AUDIO_ID', type: 'audioPlayer/SET_ACTIVE_AUDIO_ID',
payload: { id }, payload: { id, context },
}; };
} }
@ -42,12 +51,18 @@ function setActiveAudioID(id: string | undefined): SetActiveAudioIDAction {
function getEmptyState(): AudioPlayerStateType { function getEmptyState(): AudioPlayerStateType {
return { return {
activeAudioID: undefined, activeAudioID: undefined,
activeAudioContext: undefined,
}; };
} }
export function reducer( export function reducer(
state: Readonly<AudioPlayerStateType> = getEmptyState(), state: Readonly<AudioPlayerStateType> = getEmptyState(),
action: Readonly<AudioPlayerActionType | SwitchToAssociatedViewActionType> action: Readonly<
| AudioPlayerActionType
| SwitchToAssociatedViewActionType
| MessageDeletedActionType
| MessageChangedActionType
>
): AudioPlayerStateType { ): AudioPlayerStateType {
if (action.type === 'audioPlayer/SET_ACTIVE_AUDIO_ID') { if (action.type === 'audioPlayer/SET_ACTIVE_AUDIO_ID') {
const { payload } = action; const { payload } = action;
@ -55,6 +70,7 @@ export function reducer(
return { return {
...state, ...state,
activeAudioID: payload.id, activeAudioID: payload.id,
activeAudioContext: payload.context,
}; };
} }
@ -66,5 +82,36 @@ export function reducer(
}; };
} }
// Reset activeAudioID on when played message is deleted on expiration.
if (action.type === 'MESSAGE_DELETED') {
const { id } = action.payload;
if (state.activeAudioID !== id) {
return state;
}
return {
...state,
activeAudioID: undefined,
};
}
// Reset activeAudioID on when played message is deleted for everyone.
if (action.type === 'MESSAGE_CHANGED') {
const { id, data } = action.payload;
if (state.activeAudioID !== id) {
return state;
}
if (!data.deletedForEveryone) {
return state;
}
return {
...state,
activeAudioID: undefined,
};
}
return state; return state;
} }

View file

@ -7,9 +7,12 @@ import { Provider } from 'react-redux';
import { Store } from 'redux'; import { Store } from 'redux';
import { SmartApp } from '../smart/App'; import { SmartApp } from '../smart/App';
import { SmartGlobalAudioProvider } from '../smart/GlobalAudioProvider';
export const createApp = (store: Store): ReactElement => ( export const createApp = (store: Store): ReactElement => (
<Provider store={store}> <Provider store={store}>
<SmartApp /> <SmartGlobalAudioProvider>
<SmartApp />
</SmartGlobalAudioProvider>
</Provider> </Provider>
); );

View file

@ -0,0 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { StateType } from '../reducer';
export const isPaused = (state: StateType): boolean => {
return state.audioPlayer.activeAudioID === undefined;
};

View file

@ -309,7 +309,7 @@ export function getPropsForMessage(
readReceiptSetting: boolean, readReceiptSetting: boolean,
regionCode: string, regionCode: string,
accountSelector: (identifier?: string) => boolean accountSelector: (identifier?: string) => boolean
): PropsForMessage { ): Omit<PropsForMessage, 'renderingContext'> {
const contact = getContact( const contact = getContact(
message, message,
conversationSelector, conversationSelector,

View file

@ -0,0 +1,20 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { GlobalAudioProvider } from '../../components/GlobalAudioContext';
import { StateType } from '../reducer';
import { isPaused } from '../selectors/audioPlayer';
import { getSelectedConversationId } from '../selectors/conversations';
const mapStateToProps = (state: StateType) => {
return {
conversationId: getSelectedConversationId(state),
isPaused: isPaused(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGlobalAudioProvider = smart(GlobalAudioProvider);

View file

@ -16,6 +16,7 @@ export type Props = {
direction?: 'incoming' | 'outgoing'; direction?: 'incoming' | 'outgoing';
id: string; id: string;
renderingContext: string;
i18n: LocalizerType; i18n: LocalizerType;
attachment: AttachmentType; attachment: AttachmentType;
withContentAbove: boolean; withContentAbove: boolean;

View file

@ -24,7 +24,7 @@ export { Contact } from '../../components/conversation/MessageDetail';
export type OwnProps = { export type OwnProps = {
contacts: Array<Contact>; contacts: Array<Contact>;
errors: Array<Error>; errors: Array<Error>;
message: MessagePropsDataType; message: Omit<MessagePropsDataType, 'renderingContext'>;
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;

View file

@ -77,6 +77,7 @@ function renderItem(
function renderLastSeenIndicator(id: string): JSX.Element { function renderLastSeenIndicator(id: string): JSX.Element {
return <SmartLastSeenIndicator id={id} />; return <SmartLastSeenIndicator id={id} />;
} }
function renderHeroRow( function renderHeroRow(
id: string, id: string,
onHeightChange: () => unknown, onHeightChange: () => unknown,

View file

@ -4,22 +4,93 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { actions } from '../../../state/ducks/audioPlayer'; import { actions } from '../../../state/ducks/audioPlayer';
import {
actions as conversationsActions,
SwitchToAssociatedViewActionType,
} from '../../../state/ducks/conversations';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
import { StateType, reducer as rootReducer } from '../../../state/reducer'; import { StateType, reducer as rootReducer } from '../../../state/reducer';
const { messageDeleted, messageChanged } = conversationsActions;
const MESSAGE_ID = 'message-id';
describe('both/state/ducks/audioPlayer', () => { describe('both/state/ducks/audioPlayer', () => {
const getEmptyRootState = (): StateType => { const getEmptyRootState = (): StateType => {
return rootReducer(undefined, noopAction()); return rootReducer(undefined, noopAction());
}; };
const getInitializedState = (): StateType => {
const state = getEmptyRootState();
const updated = rootReducer(
state,
actions.setActiveAudioID(MESSAGE_ID, 'context')
);
assert.strictEqual(updated.audioPlayer.activeAudioID, MESSAGE_ID);
assert.strictEqual(updated.audioPlayer.activeAudioContext, 'context');
return updated;
};
describe('setActiveAudioID', () => { describe('setActiveAudioID', () => {
it("updates `activeAudioID` in the audioPlayer's state", () => { it("updates `activeAudioID` in the audioPlayer's state", () => {
const state = getEmptyRootState(); const state = getEmptyRootState();
assert.strictEqual(state.audioPlayer.activeAudioID, undefined); assert.strictEqual(state.audioPlayer.activeAudioID, undefined);
const updated = rootReducer(state, actions.setActiveAudioID('test')); const updated = rootReducer(
state,
actions.setActiveAudioID('test', 'context')
);
assert.strictEqual(updated.audioPlayer.activeAudioID, 'test'); assert.strictEqual(updated.audioPlayer.activeAudioID, 'test');
assert.strictEqual(updated.audioPlayer.activeAudioContext, 'context');
}); });
}); });
it('resets activeAudioID when changing the conversation', () => {
const state = getInitializedState();
const updated = rootReducer(state, <SwitchToAssociatedViewActionType>{
type: 'SWITCH_TO_ASSOCIATED_VIEW',
payload: { conversationId: 'any' },
});
assert.strictEqual(updated.audioPlayer.activeAudioID, undefined);
assert.strictEqual(updated.audioPlayer.activeAudioContext, 'context');
});
it('resets activeAudioID when message was deleted', () => {
const state = getInitializedState();
const updated = rootReducer(
state,
messageDeleted(MESSAGE_ID, 'conversation-id')
);
assert.strictEqual(updated.audioPlayer.activeAudioID, undefined);
assert.strictEqual(updated.audioPlayer.activeAudioContext, 'context');
});
it('resets activeAudioID when message was erased', () => {
const state = getInitializedState();
const updated = rootReducer(
state,
messageChanged(MESSAGE_ID, 'conversation-id', {
id: MESSAGE_ID,
type: 'incoming',
sent_at: 1,
received_at: 1,
timestamp: 1,
conversationId: 'conversation-id',
deletedForEveryone: true,
})
);
assert.strictEqual(updated.audioPlayer.activeAudioID, undefined);
assert.strictEqual(updated.audioPlayer.activeAudioContext, 'context');
});
}); });

View file

@ -0,0 +1,32 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { actions } from '../../../state/ducks/audioPlayer';
import { noopAction } from '../../../state/ducks/noop';
import { isPaused } from '../../../state/selectors/audioPlayer';
import { StateType, reducer as rootReducer } from '../../../state/reducer';
describe('state/selectors/audioPlayer', () => {
const getEmptyRootState = (): StateType => {
return rootReducer(undefined, noopAction());
};
describe('isPaused', () => {
it('returns true if state.audioPlayer.activeAudioID is undefined', () => {
const state = getEmptyRootState();
assert.isTrue(isPaused(state));
});
it('returns false if state.audioPlayer.activeAudioID is not undefined', () => {
const state = getEmptyRootState();
const updated = rootReducer(
state,
actions.setActiveAudioID('id', 'context')
);
assert.isFalse(isPaused(updated));
});
});
});