From 05f59f3db1958a0acf88d4e91aa4e8498b0a0513 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Mon, 15 Mar 2021 17:59:48 -0700 Subject: [PATCH] Add download button and pending spinner for audio messages --- _locales/en/messages.json | 8 + images/icons/v2/arrow-down-20.svg | 1 + images/icons/v2/audio-spinner-arc-22.svg | 1 + stylesheets/components/MessageAudio.scss | 37 +-- .../conversation/Message.stories.tsx | 33 +++ ts/components/conversation/Message.tsx | 15 +- ts/components/conversation/MessageAudio.tsx | 241 ++++++++++++------ ts/state/smart/MessageAudio.tsx | 4 +- ts/util/lint/exceptions.json | 8 +- 9 files changed, 246 insertions(+), 102 deletions(-) create mode 100644 images/icons/v2/arrow-down-20.svg create mode 100644 images/icons/v2/audio-spinner-arc-22.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5fcecf597..585136922 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -5103,6 +5103,14 @@ "message": "Pause audio attachment", "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": { "message": "Playback time of audio attachment", "description": "Aria label for audio attachment's playback time slider" diff --git a/images/icons/v2/arrow-down-20.svg b/images/icons/v2/arrow-down-20.svg new file mode 100644 index 000000000..ab05d2080 --- /dev/null +++ b/images/icons/v2/arrow-down-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/audio-spinner-arc-22.svg b/images/icons/v2/audio-spinner-arc-22.svg new file mode 100644 index 000000000..73d0d7d21 --- /dev/null +++ b/images/icons/v2/audio-spinner-arc-22.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stylesheets/components/MessageAudio.scss b/stylesheets/components/MessageAudio.scss index c0b80ae61..dfb7befcf 100644 --- a/stylesheets/components/MessageAudio.scss +++ b/stylesheets/components/MessageAudio.scss @@ -59,7 +59,8 @@ margin-top: 6px; } -.module-message__audio-attachment__button { +.module-message__audio-attachment__button, +.module-message__audio-attachment__spinner { flex-shrink: 0; width: 36px; height: 36px; @@ -75,22 +76,28 @@ content: ''; } - @mixin audio-icon($name, $color) { + @mixin audio-icon($name, $icon, $color) { &--#{$name}::before { - @include color-svg( - '../images/icons/v2/#{$name}-solid-20.svg', - $color, - false - ); + @include color-svg('../images/icons/v2/#{$icon}.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 & { @mixin android { background: $color-white-alpha-20; - @include audio-icon(play, $color-white); - @include audio-icon(pause, $color-white); + @include all-audio-icons($color-white); } @include light-theme { @@ -102,14 +109,12 @@ @include ios-theme { background: $color-white; - @include audio-icon(play, $color-gray-60); - @include audio-icon(pause, $color-gray-60); + @include all-audio-icons($color-gray-60); } @include ios-dark-theme { background: $color-gray-60; - @include audio-icon(play, $color-gray-15); - @include audio-icon(pause, $color-gray-15); + @include all-audio-icons($color-gray-15); } } @@ -117,15 +122,13 @@ @mixin android { background: $color-white; - @include audio-icon(play, $color-gray-60); - @include audio-icon(pause, $color-gray-60); + @include all-audio-icons($color-gray-60); } @mixin ios { background: $color-white-alpha-20; - @include audio-icon(play, $color-white); - @include audio-icon(pause, $color-white); + @include all-audio-icons($color-white); } @include light-theme { diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 87c591a46..bc88775df 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -753,6 +753,39 @@ story.add('Audio with Caption', () => { 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', () => { const props = createProps({ attachments: [ diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 37f4f76f7..8894788d5 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -85,9 +85,11 @@ export type AudioAttachmentProps = { buttonRef: React.RefObject; direction: DirectionType; theme: ThemeType | undefined; - url: string; + attachment: AttachmentType; withContentAbove: boolean; withContentBelow: boolean; + + kickOffAttachmentDownload(): void; }; export type PropsData = { @@ -754,16 +756,23 @@ export class Message extends React.PureComponent { ); } - if (!firstAttachment.pending && isAudio(attachments)) { + if (isAudio(attachments)) { return renderAudioAttachment({ i18n, buttonRef: this.audioButtonRef, id, direction, theme, - url: firstAttachment.url, + attachment: firstAttachment, withContentAbove, withContentBelow, + + kickOffAttachmentDownload() { + kickOffAttachmentDownload({ + attachment: firstAttachment, + messageId: id, + }); + }, }); } const { pending, fileName, fileSize, contentType } = firstAttachment; diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index 1492d28c7..849520a36 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -8,12 +8,13 @@ import { noop } from 'lodash'; import { assert } from '../../util/assert'; import { LocalizerType } from '../../types/Util'; import { WaveformCache } from '../../types/Audio'; +import { hasNotDownloaded, AttachmentType } from '../../types/Attachment'; export type Props = { direction?: 'incoming' | 'outgoing'; id: string; i18n: LocalizerType; - url: string; + attachment: AttachmentType; withContentAbove: boolean; withContentBelow: boolean; @@ -23,11 +24,21 @@ export type Props = { waveformCache: WaveformCache; buttonRef: React.RefObject; + kickOffAttachmentDownload(): void; activeAudioID: string | undefined; setActiveAudioID: (id: string | undefined) => void; }; +type ButtonProps = { + i18n: LocalizerType; + buttonRef: React.RefObject; + + mod: string; + label: string; + onClick: () => void; +}; + type LoadAudioOptions = { audioContext: AudioContext; waveformCache: WaveformCache; @@ -39,10 +50,17 @@ type LoadAudioResult = { peaks: ReadonlyArray; }; +enum State { + NotDownloaded = 'NotDownloaded', + Pending = 'Pending', + Normal = 'Normal', +} + // Constants const CSS_BASE = 'module-message__audio-attachment'; const PEAK_COUNT = 47; +const BAR_NOT_DOWNLOADED_HEIGHT = 2; const BAR_MIN_HEIGHT = 4; const BAR_MAX_HEIGHT = 20; @@ -130,6 +148,43 @@ async function loadAudio(options: LoadAudioOptions): Promise { return result; } +const Button: React.FC = 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 ( +