Add download button and pending spinner for audio messages
This commit is contained in:
parent
f98c3cba8c
commit
05f59f3db1
9 changed files with 246 additions and 102 deletions
|
@ -5103,6 +5103,14 @@
|
||||||
"message": "Pause audio attachment",
|
"message": "Pause audio attachment",
|
||||||
"description": "Aria label for audio attachment's Pause button"
|
"description": "Aria label for audio attachment's Pause button"
|
||||||
},
|
},
|
||||||
|
"MessageAudio--download": {
|
||||||
|
"message": "Download audio attachment",
|
||||||
|
"description": "Aria label for audio attachment's Download button"
|
||||||
|
},
|
||||||
|
"MessageAudio--pending": {
|
||||||
|
"message": "Downloading audio attachment...",
|
||||||
|
"description": "Aria label for pending audio attachment spinner"
|
||||||
|
},
|
||||||
"MessageAudio--slider": {
|
"MessageAudio--slider": {
|
||||||
"message": "Playback time of audio attachment",
|
"message": "Playback time of audio attachment",
|
||||||
"description": "Aria label for audio attachment's playback time slider"
|
"description": "Aria label for audio attachment's playback time slider"
|
||||||
|
|
1
images/icons/v2/arrow-down-20.svg
Normal file
1
images/icons/v2/arrow-down-20.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="m2.486 10.5 1.061-1.061 4.885 4.886.804 1.125V3h1.5v12.45l.759-1.062 4.963-4.92 1.056 1.064-7.53 7.466L2.486 10.5z"/></svg>
|
After Width: | Height: | Size: 195 B |
1
images/icons/v2/audio-spinner-arc-22.svg
Normal file
1
images/icons/v2/audio-spinner-arc-22.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="22" height="22" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="22" height="22"><path fill-rule="evenodd" clip-rule="evenodd" d="M22 0H0v22h11V11h11V0z" fill="#C4C4C4"/></mask><g mask="url(#a)"><circle cx="11" cy="11" r="9.75" stroke="#5E5E5E" stroke-width="2.5"/></g></svg>
|
After Width: | Height: | Size: 362 B |
|
@ -59,7 +59,8 @@
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__audio-attachment__button {
|
.module-message__audio-attachment__button,
|
||||||
|
.module-message__audio-attachment__spinner {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
@ -75,22 +76,28 @@
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin audio-icon($name, $color) {
|
@mixin audio-icon($name, $icon, $color) {
|
||||||
&--#{$name}::before {
|
&--#{$name}::before {
|
||||||
@include color-svg(
|
@include color-svg('../images/icons/v2/#{$icon}.svg', $color, false);
|
||||||
'../images/icons/v2/#{$name}-solid-20.svg',
|
|
||||||
$color,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin all-audio-icons($color) {
|
||||||
|
@include audio-icon(play, play-solid-20, $color);
|
||||||
|
@include audio-icon(pause, pause-solid-20, $color);
|
||||||
|
@include audio-icon(download, arrow-down-20, $color);
|
||||||
|
@include audio-icon(pending, audio-spinner-arc-22, $color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending::before {
|
||||||
|
animation: spinner-arc-animation 1000ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__audio-attachment--incoming & {
|
.module-message__audio-attachment--incoming & {
|
||||||
@mixin android {
|
@mixin android {
|
||||||
background: $color-white-alpha-20;
|
background: $color-white-alpha-20;
|
||||||
|
|
||||||
@include audio-icon(play, $color-white);
|
@include all-audio-icons($color-white);
|
||||||
@include audio-icon(pause, $color-white);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
|
@ -102,14 +109,12 @@
|
||||||
@include ios-theme {
|
@include ios-theme {
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
|
|
||||||
@include audio-icon(play, $color-gray-60);
|
@include all-audio-icons($color-gray-60);
|
||||||
@include audio-icon(pause, $color-gray-60);
|
|
||||||
}
|
}
|
||||||
@include ios-dark-theme {
|
@include ios-dark-theme {
|
||||||
background: $color-gray-60;
|
background: $color-gray-60;
|
||||||
|
|
||||||
@include audio-icon(play, $color-gray-15);
|
@include all-audio-icons($color-gray-15);
|
||||||
@include audio-icon(pause, $color-gray-15);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,15 +122,13 @@
|
||||||
@mixin android {
|
@mixin android {
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
|
|
||||||
@include audio-icon(play, $color-gray-60);
|
@include all-audio-icons($color-gray-60);
|
||||||
@include audio-icon(pause, $color-gray-60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin ios {
|
@mixin ios {
|
||||||
background: $color-white-alpha-20;
|
background: $color-white-alpha-20;
|
||||||
|
|
||||||
@include audio-icon(play, $color-white);
|
@include all-audio-icons($color-white);
|
||||||
@include audio-icon(pause, $color-white);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
|
|
|
@ -753,6 +753,39 @@ story.add('Audio with Caption', () => {
|
||||||
return renderBothDirections(props);
|
return renderBothDirections(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Audio with Not Downloaded Attachment', () => {
|
||||||
|
const props = createProps({
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: AUDIO_MP3,
|
||||||
|
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
url: undefined as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: 'sent',
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Audio with Pending Attachment', () => {
|
||||||
|
const props = createProps({
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: AUDIO_MP3,
|
||||||
|
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
url: undefined as any,
|
||||||
|
pending: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: 'sent',
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
story.add('Other File Type', () => {
|
story.add('Other File Type', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
attachments: [
|
attachments: [
|
||||||
|
|
|
@ -85,9 +85,11 @@ export type AudioAttachmentProps = {
|
||||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||||
direction: DirectionType;
|
direction: DirectionType;
|
||||||
theme: ThemeType | undefined;
|
theme: ThemeType | undefined;
|
||||||
url: string;
|
attachment: AttachmentType;
|
||||||
withContentAbove: boolean;
|
withContentAbove: boolean;
|
||||||
withContentBelow: boolean;
|
withContentBelow: boolean;
|
||||||
|
|
||||||
|
kickOffAttachmentDownload(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
|
@ -754,16 +756,23 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!firstAttachment.pending && isAudio(attachments)) {
|
if (isAudio(attachments)) {
|
||||||
return renderAudioAttachment({
|
return renderAudioAttachment({
|
||||||
i18n,
|
i18n,
|
||||||
buttonRef: this.audioButtonRef,
|
buttonRef: this.audioButtonRef,
|
||||||
id,
|
id,
|
||||||
direction,
|
direction,
|
||||||
theme,
|
theme,
|
||||||
url: firstAttachment.url,
|
attachment: firstAttachment,
|
||||||
withContentAbove,
|
withContentAbove,
|
||||||
withContentBelow,
|
withContentBelow,
|
||||||
|
|
||||||
|
kickOffAttachmentDownload() {
|
||||||
|
kickOffAttachmentDownload({
|
||||||
|
attachment: firstAttachment,
|
||||||
|
messageId: id,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||||
|
|
|
@ -8,12 +8,13 @@ import { noop } from 'lodash';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { WaveformCache } from '../../types/Audio';
|
import { WaveformCache } from '../../types/Audio';
|
||||||
|
import { hasNotDownloaded, AttachmentType } from '../../types/Attachment';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
direction?: 'incoming' | 'outgoing';
|
direction?: 'incoming' | 'outgoing';
|
||||||
id: string;
|
id: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
url: string;
|
attachment: AttachmentType;
|
||||||
withContentAbove: boolean;
|
withContentAbove: boolean;
|
||||||
withContentBelow: boolean;
|
withContentBelow: boolean;
|
||||||
|
|
||||||
|
@ -23,11 +24,21 @@ export type Props = {
|
||||||
waveformCache: WaveformCache;
|
waveformCache: WaveformCache;
|
||||||
|
|
||||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||||
|
kickOffAttachmentDownload(): void;
|
||||||
|
|
||||||
activeAudioID: string | undefined;
|
activeAudioID: string | undefined;
|
||||||
setActiveAudioID: (id: string | undefined) => void;
|
setActiveAudioID: (id: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ButtonProps = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||||
|
|
||||||
|
mod: string;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
type LoadAudioOptions = {
|
type LoadAudioOptions = {
|
||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
waveformCache: WaveformCache;
|
waveformCache: WaveformCache;
|
||||||
|
@ -39,10 +50,17 @@ type LoadAudioResult = {
|
||||||
peaks: ReadonlyArray<number>;
|
peaks: ReadonlyArray<number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
NotDownloaded = 'NotDownloaded',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Normal = 'Normal',
|
||||||
|
}
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
|
|
||||||
const CSS_BASE = 'module-message__audio-attachment';
|
const CSS_BASE = 'module-message__audio-attachment';
|
||||||
const PEAK_COUNT = 47;
|
const PEAK_COUNT = 47;
|
||||||
|
const BAR_NOT_DOWNLOADED_HEIGHT = 2;
|
||||||
const BAR_MIN_HEIGHT = 4;
|
const BAR_MIN_HEIGHT = 4;
|
||||||
const BAR_MAX_HEIGHT = 20;
|
const BAR_MAX_HEIGHT = 20;
|
||||||
|
|
||||||
|
@ -130,6 +148,43 @@ async function loadAudio(options: LoadAudioOptions): Promise<LoadAudioResult> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = props => {
|
||||||
|
const { i18n, buttonRef, mod, label, onClick } = props;
|
||||||
|
// Clicking button toggle playback
|
||||||
|
const onButtonClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard playback toggle
|
||||||
|
const onButtonKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
ref={buttonRef}
|
||||||
|
className={classNames(
|
||||||
|
`${CSS_BASE}__button`,
|
||||||
|
`${CSS_BASE}__button--${mod}`
|
||||||
|
)}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
onKeyDown={onButtonKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={i18n(label)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display message audio attachment along with its waveform, duration, and
|
* Display message audio attachment along with its waveform, duration, and
|
||||||
* toggle Play/Pause button.
|
* toggle Play/Pause button.
|
||||||
|
@ -147,11 +202,12 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
direction,
|
direction,
|
||||||
url,
|
attachment,
|
||||||
withContentAbove,
|
withContentAbove,
|
||||||
withContentBelow,
|
withContentBelow,
|
||||||
|
|
||||||
buttonRef,
|
buttonRef,
|
||||||
|
kickOffAttachmentDownload,
|
||||||
|
|
||||||
audio,
|
audio,
|
||||||
audioContext,
|
audioContext,
|
||||||
|
@ -179,10 +235,20 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
new Array(PEAK_COUNT).fill(0)
|
new Array(PEAK_COUNT).fill(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let state: State;
|
||||||
|
|
||||||
|
if (attachment.pending) {
|
||||||
|
state = State.Pending;
|
||||||
|
} else if (hasNotDownloaded(attachment)) {
|
||||||
|
state = State.NotDownloaded;
|
||||||
|
} else {
|
||||||
|
state = State.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
// This effect loads audio file and computes its RMS peak for dispalying the
|
// This effect loads audio file and computes its RMS peak for dispalying the
|
||||||
// waveform.
|
// waveform.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading) {
|
if (!isLoading || state !== State.Normal) {
|
||||||
return noop;
|
return noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +259,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
const { peaks: newPeaks, duration: newDuration } = await loadAudio({
|
const { peaks: newPeaks, duration: newDuration } = await loadAudio({
|
||||||
audioContext,
|
audioContext,
|
||||||
waveformCache,
|
waveformCache,
|
||||||
url,
|
url: attachment.url,
|
||||||
});
|
});
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
return;
|
return;
|
||||||
|
@ -212,7 +278,15 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
return () => {
|
return () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
};
|
};
|
||||||
}, [url, isLoading, setPeaks, setDuration, audioContext, waveformCache]);
|
}, [
|
||||||
|
attachment,
|
||||||
|
audioContext,
|
||||||
|
isLoading,
|
||||||
|
setDuration,
|
||||||
|
setPeaks,
|
||||||
|
state,
|
||||||
|
waveformCache,
|
||||||
|
]);
|
||||||
|
|
||||||
// This effect attaches/detaches event listeners to the global <audio/>
|
// This effect attaches/detaches event listeners to the global <audio/>
|
||||||
// instance that we reuse from the GlobalAudioContext.
|
// instance that we reuse from the GlobalAudioContext.
|
||||||
|
@ -300,34 +374,19 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
audio.pause();
|
audio.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.src = url;
|
audio.src = attachment.url;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clicking button toggle playback
|
|
||||||
const onClick = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
toggleIsPlaying();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keyboard playback toggle
|
|
||||||
const onKeyDown = (event: React.KeyboardEvent) => {
|
|
||||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
toggleIsPlaying();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clicking waveform moves playback head position and starts playback.
|
// Clicking waveform moves playback head position and starts playback.
|
||||||
const onWaveformClick = (event: React.MouseEvent) => {
|
const onWaveformClick = (event: React.MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (state !== State.Normal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
toggleIsPlaying();
|
toggleIsPlaying();
|
||||||
}
|
}
|
||||||
|
@ -381,33 +440,9 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonLabel = i18n(
|
|
||||||
isPlaying ? 'MessageAudio--play' : 'MessageAudio--pause'
|
|
||||||
);
|
|
||||||
|
|
||||||
const peakPosition = peaks.length * (currentTime / duration);
|
const peakPosition = peaks.length * (currentTime / duration);
|
||||||
|
|
||||||
return (
|
const waveform = (
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
CSS_BASE,
|
|
||||||
`${CSS_BASE}--${direction}`,
|
|
||||||
withContentBelow ? `${CSS_BASE}--with-content-below` : null,
|
|
||||||
withContentAbove ? `${CSS_BASE}--with-content-above` : null
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classNames(
|
|
||||||
`${CSS_BASE}__button`,
|
|
||||||
`${CSS_BASE}__button--${isPlaying ? 'pause' : 'play'}`
|
|
||||||
)}
|
|
||||||
ref={buttonRef}
|
|
||||||
onClick={onClick}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={buttonLabel}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
ref={waveformRef}
|
ref={waveformRef}
|
||||||
className={`${CSS_BASE}__waveform`}
|
className={`${CSS_BASE}__waveform`}
|
||||||
|
@ -424,6 +459,10 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
>
|
>
|
||||||
{peaks.map((peak, i) => {
|
{peaks.map((peak, i) => {
|
||||||
let height = Math.max(BAR_MIN_HEIGHT, BAR_MAX_HEIGHT * peak);
|
let height = Math.max(BAR_MIN_HEIGHT, BAR_MAX_HEIGHT * peak);
|
||||||
|
if (state !== State.Normal) {
|
||||||
|
height = BAR_NOT_DOWNLOADED_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
const highlight = i < peakPosition;
|
const highlight = i < peakPosition;
|
||||||
|
|
||||||
// Use maximum height for current audio position
|
// Use maximum height for current audio position
|
||||||
|
@ -445,6 +484,54 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
let button: React.ReactElement;
|
||||||
|
if (state === State.Pending) {
|
||||||
|
// Not really a button, but who cares?
|
||||||
|
button = (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`${CSS_BASE}__spinner`,
|
||||||
|
`${CSS_BASE}__spinner--pending`
|
||||||
|
)}
|
||||||
|
title={i18n('MessageAudio--pending')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (state === State.NotDownloaded) {
|
||||||
|
button = (
|
||||||
|
<Button
|
||||||
|
i18n={i18n}
|
||||||
|
buttonRef={buttonRef}
|
||||||
|
mod="download"
|
||||||
|
label="MessageAudio--download"
|
||||||
|
onClick={kickOffAttachmentDownload}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// State.Normal
|
||||||
|
button = (
|
||||||
|
<Button
|
||||||
|
i18n={i18n}
|
||||||
|
buttonRef={buttonRef}
|
||||||
|
mod={isPlaying ? 'pause' : 'play'}
|
||||||
|
label={isPlaying ? 'MessageAudio--pause' : 'MessageAudio--play'}
|
||||||
|
onClick={toggleIsPlaying}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
CSS_BASE,
|
||||||
|
`${CSS_BASE}--${direction}`,
|
||||||
|
withContentBelow ? `${CSS_BASE}--with-content-below` : null,
|
||||||
|
withContentAbove ? `${CSS_BASE}--with-content-above` : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{button}
|
||||||
|
{waveform}
|
||||||
<div className={`${CSS_BASE}__duration`}>{timeToText(duration)}</div>
|
<div className={`${CSS_BASE}__duration`}>{timeToText(duration)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { mapDispatchToProps } from '../actions';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { WaveformCache } from '../../types/Audio';
|
import { WaveformCache } from '../../types/Audio';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
audio: HTMLAudioElement;
|
audio: HTMLAudioElement;
|
||||||
|
@ -18,11 +19,12 @@ export type Props = {
|
||||||
direction?: 'incoming' | 'outgoing';
|
direction?: 'incoming' | 'outgoing';
|
||||||
id: string;
|
id: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
url: string;
|
attachment: AttachmentType;
|
||||||
withContentAbove: boolean;
|
withContentAbove: boolean;
|
||||||
withContentBelow: boolean;
|
withContentBelow: boolean;
|
||||||
|
|
||||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||||
|
kickOffAttachmentDownload(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: Props) => {
|
const mapStateToProps = (state: StateType, props: Props) => {
|
||||||
|
|
|
@ -14694,7 +14694,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 235,
|
"lineNumber": 237,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for managing focus only"
|
"reasonDetail": "Used for managing focus only"
|
||||||
|
@ -14703,7 +14703,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
||||||
"lineNumber": 237,
|
"lineNumber": 239,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||||
|
@ -14712,7 +14712,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " > = React.createRef();",
|
"line": " > = React.createRef();",
|
||||||
"lineNumber": 241,
|
"lineNumber": 243,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||||
|
@ -14721,7 +14721,7 @@
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/MessageAudio.js",
|
"path": "ts/components/conversation/MessageAudio.js",
|
||||||
"line": " const waveformRef = react_1.useRef(null);",
|
"line": " const waveformRef = react_1.useRef(null);",
|
||||||
"lineNumber": 116,
|
"lineNumber": 143,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-09T01:19:04.057Z",
|
"updated": "2021-03-09T01:19:04.057Z",
|
||||||
"reasonDetail": "Used for obtanining the bounding box for the container"
|
"reasonDetail": "Used for obtanining the bounding box for the container"
|
||||||
|
|
Loading…
Add table
Reference in a new issue