From 966a049743bf031cba0fb001a45e1d18b4285687 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 18 Jun 2021 00:58:37 +0300 Subject: [PATCH] Audio: Add download button --- src/components/common/Audio.scss | 30 +++++++++++- src/components/common/Audio.tsx | 36 ++++++++++++-- .../mediaViewer/MediaViewerActions.tsx | 47 +++++-------------- src/hooks/useMediaDownload.ts | 37 +++++++++++++++ src/modules/helpers/messageMedia.ts | 2 + src/modules/helpers/messages.ts | 9 +++- 6 files changed, 119 insertions(+), 42 deletions(-) create mode 100644 src/hooks/useMediaDownload.ts diff --git a/src/components/common/Audio.scss b/src/components/common/Audio.scss index 1ce020c73..fb501d7ae 100644 --- a/src/components/common/Audio.scss +++ b/src/components/common/Audio.scss @@ -13,7 +13,7 @@ --color-interactive-buffered: rgba(var(--color-text-green-rgb), 0.4); // Overlays underlying inactive color .theme-dark & { - --color-text-green-rgb: 255,255,255; + --color-text-green-rgb: 255, 255, 255; --color-text-green: var(--color-white); } @@ -70,6 +70,34 @@ } } + .media-loading { + pointer-events: none; + + .interactive { + pointer-events: auto; + } + } + + .download-button { + position: absolute; + width: 0.3rem !important; + height: 0.3rem !important; + left: 1.5rem; + top: 1.5rem; + border: 2px solid var(--background-color); + z-index: 1; + + i { + font-size: 0.8rem; + } + } + + &.bigger .download-button { + left: 2rem; + top: 2rem; + border: 2px solid var(--color-background); + } + .content { align-self: center; min-width: 0; diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index c18874008..11aeb8c79 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -13,6 +13,7 @@ import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '. import { getMediaDuration, getMediaTransferState, + getMessageAudioCaption, getMessageKey, getMessageMediaFormat, getMessageMediaHash, @@ -27,6 +28,7 @@ import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgre import useShowTransition from '../../hooks/useShowTransition'; import useBuffering from '../../hooks/useBuffering'; import useAudioPlayer from '../../hooks/useAudioPlayer'; +import useMediaDownload from '../../hooks/useMediaDownload'; import useLang, { LangFn } from '../../hooks/useLang'; import Button from '../ui/Button'; @@ -123,9 +125,21 @@ const Audio: FC = ({ setIsActivated(isPlaying); }, [isPlaying]); + const { + isDownloadStarted, + downloadProgress: directDownloadProgress, + handleDownloadClick, + } = useMediaDownload(getMessageMediaHash(message, 'download'), getMessageAudioCaption(message)); + + const isLoadingForPlaying = isActivated && !isBuffered; + const { isUploading, isTransferring, transferProgress, - } = getMediaTransferState(message, uploadProgress || downloadProgress, isActivated && !isBuffered); + } = getMediaTransferState( + message, + isDownloadStarted ? directDownloadProgress : (uploadProgress || downloadProgress), + isLoadingForPlaying || isDownloadStarted, + ); const { shouldRender: shouldRenderSpinner, @@ -220,7 +234,7 @@ const Audio: FC = ({ ); const buttonClassNames = ['toggle-play']; - if (shouldRenderSpinner) { + if (isLoadingForPlaying) { buttonClassNames.push('loading'); } else if (isPlaying) { buttonClassNames.push('pause'); @@ -282,15 +296,27 @@ const Audio: FC = ({ {shouldRenderSpinner && ( -
+
)} + {audio && ( + + )} {renderingFor === 'searchResult' && renderSearchResult()} {renderingFor !== 'searchResult' && audio && renderAudio( lang, audio, isPlaying, playProgress, bufferedProgress, seekHandlers, date, @@ -309,7 +335,7 @@ function renderAudio( bufferedProgress: number, seekHandlers: ISeekMethods, date?: number, - handleDateClick?: () => void, + handleDateClick?: NoneToVoidFunction, ) { const { title, performer, duration, fileName, diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index aaf8da5c0..50a2aa57d 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -1,13 +1,9 @@ -import React, { - FC, useCallback, useEffect, useMemo, useState, -} from '../../lib/teact/teact'; +import React, { FC, useMemo } from '../../lib/teact/teact'; import { ApiMessage } from '../../api/types'; import { IS_MOBILE_SCREEN } from '../../util/environment'; -import download from '../../util/download'; import { getMessageMediaHash } from '../../modules/helpers'; -import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress'; import useLang from '../../hooks/useLang'; import Button from '../ui/Button'; @@ -16,6 +12,7 @@ import MenuItem from '../ui/MenuItem'; import ProgressSpinner from '../ui/ProgressSpinner'; import './MediaViewerActions.scss'; +import useMediaDownload from '../../hooks/useMediaDownload'; type OwnProps = { mediaData?: string; @@ -40,29 +37,11 @@ const MediaViewerActions: FC = ({ onForward, onZoomToggle, }) => { - const [isVideoDownloadAllowed, setIsVideoDownloadAllowed] = useState(false); - const videoMediaHash = isVideo && message ? getMessageMediaHash(message, 'download') : undefined; const { - mediaData: videoBlobUrl, downloadProgress, - } = useMediaWithDownloadProgress(videoMediaHash, !isVideoDownloadAllowed); - - // Download with browser when fully loaded - useEffect(() => { - if (isVideoDownloadAllowed && videoBlobUrl) { - download(videoBlobUrl, fileName!); - setIsVideoDownloadAllowed(false); - } - }, [fileName, videoBlobUrl, isVideoDownloadAllowed]); - - // Cancel download on slide change - useEffect(() => { - setIsVideoDownloadAllowed(false); - }, [videoMediaHash]); - - const handleVideoDownloadClick = useCallback((e: React.SyntheticEvent) => { - e.stopPropagation(); - setIsVideoDownloadAllowed((isAllowed) => !isAllowed); - }, []); + isDownloadStarted, + downloadProgress, + handleDownloadClick, + } = useMediaDownload(message && isVideo ? getMessageMediaHash(message, 'download') : undefined); const lang = useLang(); @@ -98,10 +77,10 @@ const MediaViewerActions: FC = ({ )} {isVideo ? ( - {isVideoDownloadAllowed ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'} + {isDownloadStarted ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'} ) : ( = ({ )} - {isVideoDownloadAllowed && } + {isDownloadStarted && }
); } @@ -139,10 +118,10 @@ const MediaViewerActions: FC = ({ size="smaller" color="translucent-white" ariaLabel={lang('AccActionDownload')} - onClick={handleVideoDownloadClick} + onClick={handleDownloadClick} > - {isVideoDownloadAllowed ? ( - + {isDownloadStarted ? ( + ) : ( )} diff --git a/src/hooks/useMediaDownload.ts b/src/hooks/useMediaDownload.ts new file mode 100644 index 000000000..4f699b1c1 --- /dev/null +++ b/src/hooks/useMediaDownload.ts @@ -0,0 +1,37 @@ +import React, { useCallback, useEffect, useState } from '../lib/teact/teact'; + +import useMediaWithDownloadProgress from './useMediaWithDownloadProgress'; +import download from '../util/download'; + +export default function useMediaDownload( + mediaHash?: string, + fileName?: string, +) { + const [isDownloadStarted, setIsDownloadStarted] = useState(false); + + const { mediaData, downloadProgress } = useMediaWithDownloadProgress(mediaHash, !isDownloadStarted); + + // Download with browser when fully loaded + useEffect(() => { + if (isDownloadStarted && mediaData) { + download(mediaData, fileName!); + setIsDownloadStarted(false); + } + }, [fileName, mediaData, isDownloadStarted]); + + // Cancel download on source change + useEffect(() => { + setIsDownloadStarted(false); + }, [mediaHash]); + + const handleDownloadClick = useCallback((e: React.SyntheticEvent) => { + e.stopPropagation(); + setIsDownloadStarted((isAllowed) => !isAllowed); + }, []); + + return { + isDownloadStarted, + downloadProgress, + handleDownloadClick, + }; +} diff --git a/src/modules/helpers/messageMedia.ts b/src/modules/helpers/messageMedia.ts index 849dda2e0..9c75ee8bd 100644 --- a/src/modules/helpers/messageMedia.ts +++ b/src/modules/helpers/messageMedia.ts @@ -196,6 +196,8 @@ export function getMessageMediaHash( case 'micro': case 'pictogram': return undefined; + case 'download': + return `${base}?download`; default: return getVideoOrAudioBaseHash(audio, base); } diff --git a/src/modules/helpers/messages.ts b/src/modules/helpers/messages.ts index 31ccbc94f..4877b3330 100644 --- a/src/modules/helpers/messages.ts +++ b/src/modules/helpers/messages.ts @@ -49,8 +49,7 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji } if (audio) { - const caption = [audio.title, audio.performer].filter(Boolean).join(' — ') || (text && text.text); - return `${noEmoji ? '' : '🎧 '}${caption || lang('AttachMusic')}`; + return `${noEmoji ? '' : '🎧 '}${getMessageAudioCaption(message) || lang('AttachMusic')}`; } if (voice) { @@ -216,3 +215,9 @@ export function isMessageLocal(message: ApiMessage) { export function isHistoryClearMessage(message: ApiMessage) { return message.content.action && message.content.action.type === 'historyClear'; } + +export function getMessageAudioCaption(message: ApiMessage) { + const { audio, text } = message.content; + + return (audio && [audio.title, audio.performer].filter(Boolean).join(' — ')) || (text && text.text); +}