Unify audio playback under App component
This commit is contained in:
parent
8b30fc17cd
commit
2cd4160422
19 changed files with 290 additions and 80 deletions
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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: [],
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
8
ts/state/selectors/audioPlayer.ts
Normal file
8
ts/state/selectors/audioPlayer.ts
Normal 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;
|
||||||
|
};
|
|
@ -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,
|
||||||
|
|
20
ts/state/smart/GlobalAudioProvider.tsx
Normal file
20
ts/state/smart/GlobalAudioProvider.tsx
Normal 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);
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
32
ts/test-electron/state/selectors/audioPlayer_test.ts
Normal file
32
ts/test-electron/state/selectors/audioPlayer_test.ts
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue