Message: Support downloading all media with context menu and select mode (#1397)
This commit is contained in:
parent
d668e265b5
commit
a3668e8b3d
@ -1,6 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useEffect, useMemo, useRef, useState,
|
FC, memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiAudio, ApiMediaFormat, ApiMessage, ApiVoice,
|
ApiAudio, ApiMediaFormat, ApiMessage, ApiVoice,
|
||||||
@ -12,7 +13,6 @@ import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '.
|
|||||||
import {
|
import {
|
||||||
getMediaDuration,
|
getMediaDuration,
|
||||||
getMediaTransferState,
|
getMediaTransferState,
|
||||||
getMessageAudioCaption,
|
|
||||||
getMessageMediaFormat,
|
getMessageMediaFormat,
|
||||||
getMessageMediaHash,
|
getMessageMediaHash,
|
||||||
isMessageLocal,
|
isMessageLocal,
|
||||||
@ -23,11 +23,10 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getFileSizeString } from './helpers/documentInfo';
|
import { getFileSizeString } from './helpers/documentInfo';
|
||||||
import { decodeWaveform, interpolateArray } from '../../util/waveform';
|
import { decodeWaveform, interpolateArray } from '../../util/waveform';
|
||||||
import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import useBuffering from '../../hooks/useBuffering';
|
import useBuffering from '../../hooks/useBuffering';
|
||||||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||||||
import useMediaDownload from '../../hooks/useMediaDownload';
|
|
||||||
import useLang, { LangFn } from '../../hooks/useLang';
|
import useLang, { LangFn } from '../../hooks/useLang';
|
||||||
import { captureEvents } from '../../util/captureEvents';
|
import { captureEvents } from '../../util/captureEvents';
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
@ -51,6 +50,7 @@ type OwnProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
isSelectable?: boolean;
|
isSelectable?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
isDownloading: boolean;
|
||||||
onPlay: (messageId: number, chatId: number) => void;
|
onPlay: (messageId: number, chatId: number) => void;
|
||||||
onReadMedia?: () => void;
|
onReadMedia?: () => void;
|
||||||
onCancelUpload?: () => void;
|
onCancelUpload?: () => void;
|
||||||
@ -74,6 +74,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
className,
|
className,
|
||||||
isSelectable,
|
isSelectable,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
isDownloading,
|
||||||
onPlay,
|
onPlay,
|
||||||
onReadMedia,
|
onReadMedia,
|
||||||
onCancelUpload,
|
onCancelUpload,
|
||||||
@ -87,18 +88,24 @@ const Audio: FC<OwnProps> = ({
|
|||||||
const seekerRef = useRef<HTMLElement>(null);
|
const seekerRef = useRef<HTMLElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { isRtl } = lang;
|
const { isRtl } = lang;
|
||||||
|
const dispatch = getDispatch();
|
||||||
|
|
||||||
const [isActivated, setIsActivated] = useState(false);
|
const [isActivated, setIsActivated] = useState(false);
|
||||||
const shouldDownload = (isActivated || PRELOAD) && lastSyncTime;
|
const shouldLoad = (isActivated || PRELOAD) && lastSyncTime;
|
||||||
const coverHash = getMessageMediaHash(message, 'pictogram');
|
const coverHash = getMessageMediaHash(message, 'pictogram');
|
||||||
const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl);
|
const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl);
|
||||||
|
|
||||||
const { mediaData, downloadProgress } = useMediaWithDownloadProgress(
|
const mediaData = useMedia(
|
||||||
getMessageMediaHash(message, 'inline'),
|
getMessageMediaHash(message, 'inline'),
|
||||||
!shouldDownload,
|
!shouldLoad,
|
||||||
getMessageMediaFormat(message, 'inline'),
|
getMessageMediaFormat(message, 'inline'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
|
getMessageMediaHash(message, 'download'),
|
||||||
|
!isDownloading,
|
||||||
|
);
|
||||||
|
|
||||||
const handleForcePlay = useCallback(() => {
|
const handleForcePlay = useCallback(() => {
|
||||||
setIsActivated(true);
|
setIsActivated(true);
|
||||||
onPlay(message.id, message.chatId);
|
onPlay(message.id, message.chatId);
|
||||||
@ -135,20 +142,14 @@ const Audio: FC<OwnProps> = ({
|
|||||||
setIsActivated(isPlaying);
|
setIsActivated(isPlaying);
|
||||||
}, [isPlaying]);
|
}, [isPlaying]);
|
||||||
|
|
||||||
const {
|
|
||||||
isDownloadStarted,
|
|
||||||
downloadProgress: directDownloadProgress,
|
|
||||||
handleDownloadClick,
|
|
||||||
} = useMediaDownload(getMessageMediaHash(message, 'download'), getMessageAudioCaption(message));
|
|
||||||
|
|
||||||
const isLoadingForPlaying = isActivated && !isBuffered;
|
const isLoadingForPlaying = isActivated && !isBuffered;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isUploading, isTransferring, transferProgress,
|
isUploading, isTransferring, transferProgress,
|
||||||
} = getMediaTransferState(
|
} = getMediaTransferState(
|
||||||
message,
|
message,
|
||||||
isDownloadStarted ? directDownloadProgress : (uploadProgress || downloadProgress),
|
uploadProgress || downloadProgress,
|
||||||
isLoadingForPlaying || isDownloadStarted,
|
isLoadingForPlaying || isDownloading,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -173,10 +174,18 @@ const Audio: FC<OwnProps> = ({
|
|||||||
}, [isPlaying, isUploading, message.id, message.chatId, onCancelUpload, onPlay, playPause, isActivated]);
|
}, [isPlaying, isUploading, message.id, message.chatId, onCancelUpload, onPlay, playPause, isActivated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying && onReadMedia && isMediaUnread) {
|
if (onReadMedia && isMediaUnread && (isPlaying || isDownloading)) {
|
||||||
onReadMedia();
|
onReadMedia();
|
||||||
}
|
}
|
||||||
}, [isPlaying, isMediaUnread, onReadMedia]);
|
}, [isPlaying, isMediaUnread, onReadMedia, isDownloading]);
|
||||||
|
|
||||||
|
const handleDownloadClick = useCallback(() => {
|
||||||
|
if (isDownloading) {
|
||||||
|
dispatch.cancelMessageMediaDownload({ message });
|
||||||
|
} else {
|
||||||
|
dispatch.downloadMessageMedia({ message });
|
||||||
|
}
|
||||||
|
}, [dispatch, isDownloading, message]);
|
||||||
|
|
||||||
const handleSeek = useCallback((e: MouseEvent | TouchEvent) => {
|
const handleSeek = useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
if (isSeeking.current && seekerRef.current) {
|
if (isSeeking.current && seekerRef.current) {
|
||||||
@ -348,15 +357,15 @@ const Audio: FC<OwnProps> = ({
|
|||||||
round
|
round
|
||||||
size="tiny"
|
size="tiny"
|
||||||
className="download-button"
|
className="download-button"
|
||||||
ariaLabel={isDownloadStarted ? 'Cancel download' : 'Download'}
|
ariaLabel={isDownloading ? 'Cancel download' : 'Download'}
|
||||||
onClick={handleDownloadClick}
|
onClick={handleDownloadClick}
|
||||||
>
|
>
|
||||||
<i className={isDownloadStarted ? 'icon-close' : 'icon-arrow-down'} />
|
<i className={isDownloading ? 'icon-close' : 'icon-arrow-down'} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{origin === AudioOrigin.Search && renderWithTitle()}
|
{origin === AudioOrigin.Search && renderWithTitle()}
|
||||||
{origin !== AudioOrigin.Search && audio && renderAudio(
|
{origin !== AudioOrigin.Search && audio && renderAudio(
|
||||||
lang, audio, duration, isPlaying, playProgress, bufferedProgress, seekerRef, (isDownloadStarted || isUploading),
|
lang, audio, duration, isPlaying, playProgress, bufferedProgress, seekerRef, (isDownloading || isUploading),
|
||||||
date, transferProgress, onDateClick ? handleDateClick : undefined,
|
date, transferProgress, onDateClick ? handleDateClick : undefined,
|
||||||
)}
|
)}
|
||||||
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
|
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useCallback, useEffect, useState, memo, useRef,
|
FC, useCallback, memo, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiMediaFormat, ApiMessage } from '../../api/types';
|
import { ApiMediaFormat, ApiMessage } from '../../api/types';
|
||||||
|
|
||||||
@ -12,9 +13,8 @@ import {
|
|||||||
isMessageDocumentVideo,
|
isMessageDocumentVideo,
|
||||||
} from '../../modules/helpers';
|
} from '../../modules/helpers';
|
||||||
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import download from '../../util/download';
|
|
||||||
|
|
||||||
import File from './File';
|
import File from './File';
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ type OwnProps = {
|
|||||||
datetime?: number;
|
datetime?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
|
isDownloading: boolean;
|
||||||
onCancelUpload?: () => void;
|
onCancelUpload?: () => void;
|
||||||
onMediaClick?: () => void;
|
onMediaClick?: () => void;
|
||||||
onDateClick?: (messageId: number, chatId: number) => void;
|
onDateClick?: (messageId: number, chatId: number) => void;
|
||||||
@ -48,6 +49,7 @@ const Document: FC<OwnProps> = ({
|
|||||||
onCancelUpload,
|
onCancelUpload,
|
||||||
onMediaClick,
|
onMediaClick,
|
||||||
onDateClick,
|
onDateClick,
|
||||||
|
isDownloading,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@ -58,16 +60,14 @@ const Document: FC<OwnProps> = ({
|
|||||||
const withMediaViewer = onMediaClick && Boolean(document.mediaType);
|
const withMediaViewer = onMediaClick && Boolean(document.mediaType);
|
||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
const dispatch = getDispatch();
|
||||||
|
|
||||||
const [isDownloadAllowed, setIsDownloadAllowed] = useState(false);
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress<ApiMediaFormat.BlobUrl>(
|
||||||
const {
|
getMessageMediaHash(message, 'download'), !isDownloading, undefined, undefined, undefined, true,
|
||||||
mediaData, downloadProgress,
|
|
||||||
} = useMediaWithDownloadProgress<ApiMediaFormat.BlobUrl>(
|
|
||||||
getMessageMediaHash(message, 'download'), !isDownloadAllowed, undefined, undefined, undefined, true,
|
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
isUploading, isTransferring, transferProgress,
|
isUploading, isTransferring, transferProgress,
|
||||||
} = getMediaTransferState(message, uploadProgress || downloadProgress, isDownloadAllowed);
|
} = getMediaTransferState(message, uploadProgress || downloadProgress, isDownloading);
|
||||||
|
|
||||||
const hasPreview = getDocumentHasPreview(document);
|
const hasPreview = getDocumentHasPreview(document);
|
||||||
const thumbDataUri = hasPreview ? getMessageMediaThumbDataUri(message) : undefined;
|
const thumbDataUri = hasPreview ? getMessageMediaThumbDataUri(message) : undefined;
|
||||||
@ -75,28 +75,29 @@ const Document: FC<OwnProps> = ({
|
|||||||
const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (withMediaViewer) {
|
if (isDownloading) {
|
||||||
onMediaClick!();
|
dispatch.cancelMessageMediaDownload({ message });
|
||||||
} else if (isUploading) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUploading) {
|
||||||
if (onCancelUpload) {
|
if (onCancelUpload) {
|
||||||
onCancelUpload();
|
onCancelUpload();
|
||||||
}
|
}
|
||||||
} else {
|
return;
|
||||||
setIsDownloadAllowed((isAllowed) => !isAllowed);
|
|
||||||
}
|
}
|
||||||
}, [withMediaViewer, isUploading, onCancelUpload, onMediaClick]);
|
|
||||||
|
if (withMediaViewer) {
|
||||||
|
onMediaClick!();
|
||||||
|
} else {
|
||||||
|
dispatch.downloadMessageMedia({ message });
|
||||||
|
}
|
||||||
|
}, [withMediaViewer, isUploading, isDownloading, onMediaClick, onCancelUpload, dispatch, message]);
|
||||||
|
|
||||||
const handleDateClick = useCallback(() => {
|
const handleDateClick = useCallback(() => {
|
||||||
onDateClick!(message.id, message.chatId);
|
onDateClick!(message.id, message.chatId);
|
||||||
}, [onDateClick, message.id, message.chatId]);
|
}, [onDateClick, message.id, message.chatId]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDownloadAllowed && mediaData) {
|
|
||||||
download(mediaData, fileName);
|
|
||||||
setIsDownloadAllowed(false);
|
|
||||||
}
|
|
||||||
}, [fileName, mediaData, isDownloadAllowed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<File
|
<File
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ const AudioResults: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
globalMessagesByChatId,
|
globalMessagesByChatId,
|
||||||
foundIds,
|
foundIds,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
activeDownloads,
|
||||||
searchMessagesGlobal,
|
searchMessagesGlobal,
|
||||||
focusMessage,
|
focusMessage,
|
||||||
openAudioPlayer,
|
openAudioPlayer,
|
||||||
@ -104,6 +105,7 @@ const AudioResults: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
className="scroll-item"
|
className="scroll-item"
|
||||||
onPlay={handlePlayAudio}
|
onPlay={handlePlayAudio}
|
||||||
onDateClick={handleMessageFocus}
|
onDateClick={handleMessageFocus}
|
||||||
|
isDownloading={activeDownloads[message.chatId]?.includes(message.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -40,6 +40,7 @@ const FileResults: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
usersById,
|
usersById,
|
||||||
globalMessagesByChatId,
|
globalMessagesByChatId,
|
||||||
foundIds,
|
foundIds,
|
||||||
|
activeDownloads,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
searchMessagesGlobal,
|
searchMessagesGlobal,
|
||||||
focusMessage,
|
focusMessage,
|
||||||
@ -94,6 +95,7 @@ const FileResults: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
sender={getSenderName(lang, message, chatsById, usersById)}
|
sender={getSenderName(lang, message, chatsById, usersById)}
|
||||||
className="scroll-item"
|
className="scroll-item"
|
||||||
onDateClick={handleMessageFocus}
|
onDateClick={handleMessageFocus}
|
||||||
|
isDownloading={activeDownloads[message.chatId]?.includes(message.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export type StateProps = {
|
|||||||
foundIds?: string[];
|
foundIds?: string[];
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
searchChatId?: number;
|
searchChatId?: number;
|
||||||
|
activeDownloads: Record<number, number[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
||||||
@ -33,6 +34,8 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
|||||||
const { byChatId: globalMessagesByChatId } = global.messages;
|
const { byChatId: globalMessagesByChatId } = global.messages;
|
||||||
const foundIds = resultsByType?.[currentType]?.foundIds;
|
const foundIds = resultsByType?.[currentType]?.foundIds;
|
||||||
|
|
||||||
|
const activeDownloads = global.activeDownloads.byChatId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
isLoading: foundIds === undefined
|
isLoading: foundIds === undefined
|
||||||
@ -42,6 +45,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
|||||||
globalMessagesByChatId,
|
globalMessagesByChatId,
|
||||||
foundIds,
|
foundIds,
|
||||||
searchChatId: chatId,
|
searchChatId: chatId,
|
||||||
|
activeDownloads,
|
||||||
lastSyncTime: global.lastSyncTime,
|
lastSyncTime: global.lastSyncTime,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { fetchBlob } from '../../../util/files';
|
|||||||
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
import useCanvasBlur from '../../../hooks/useCanvasBlur';
|
import useCanvasBlur from '../../../hooks/useCanvasBlur';
|
||||||
@ -40,15 +40,15 @@ const WallpaperTile: FC<OwnProps> = ({
|
|||||||
const {
|
const {
|
||||||
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
|
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
|
||||||
} = useTransitionForMedia(previewBlobUrl || localBlobUrl, 'slow');
|
} = useTransitionForMedia(previewBlobUrl || localBlobUrl, 'slow');
|
||||||
const [isDownloadAllowed, setIsDownloadAllowed] = useState(false);
|
const [isLoadAllowed, setIsLoadAllowed] = useState(false);
|
||||||
const {
|
const {
|
||||||
mediaData: fullMedia, downloadProgress,
|
mediaData: fullMedia, loadProgress,
|
||||||
} = useMediaWithDownloadProgress(localMediaHash, !isDownloadAllowed);
|
} = useMediaWithLoadProgress(localMediaHash, !isLoadAllowed);
|
||||||
const wasDownloadDisabled = usePrevious(isDownloadAllowed) === false;
|
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||||
const { shouldRender: shouldRenderSpinner, transitionClassNames: spinnerClassNames } = useShowTransition(
|
const { shouldRender: shouldRenderSpinner, transitionClassNames: spinnerClassNames } = useShowTransition(
|
||||||
(isDownloadAllowed && !fullMedia) || slug === UPLOADING_WALLPAPER_SLUG,
|
(isLoadAllowed && !fullMedia) || slug === UPLOADING_WALLPAPER_SLUG,
|
||||||
undefined,
|
undefined,
|
||||||
wasDownloadDisabled,
|
wasLoadDisabled,
|
||||||
'slow',
|
'slow',
|
||||||
);
|
);
|
||||||
// To prevent triggering of the effect for useCallback
|
// To prevent triggering of the effect for useCallback
|
||||||
@ -73,7 +73,7 @@ const WallpaperTile: FC<OwnProps> = ({
|
|||||||
if (fullMedia) {
|
if (fullMedia) {
|
||||||
handleSelect();
|
handleSelect();
|
||||||
} else {
|
} else {
|
||||||
setIsDownloadAllowed((isAllowed) => !isAllowed);
|
setIsLoadAllowed((isAllowed) => !isAllowed);
|
||||||
}
|
}
|
||||||
}, [fullMedia, handleSelect]);
|
}, [fullMedia, handleSelect]);
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ const WallpaperTile: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
{shouldRenderSpinner && (
|
{shouldRenderSpinner && (
|
||||||
<div className={buildClassName('spinner-container', spinnerClassNames)}>
|
<div className={buildClassName('spinner-container', spinnerClassNames)}>
|
||||||
<ProgressSpinner progress={downloadProgress} onClick={handleClick} />
|
<ProgressSpinner progress={loadProgress} onClick={handleClick} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
81
src/components/main/DownloadManager.tsx
Normal file
81
src/components/main/DownloadManager.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { FC, memo, useEffect } from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { GlobalActions, Thread } from '../../global/types';
|
||||||
|
import { ApiMediaFormat, ApiMessage } from '../../api/types';
|
||||||
|
|
||||||
|
import * as mediaLoader from '../../util/mediaLoader';
|
||||||
|
import download from '../../util/download';
|
||||||
|
import {
|
||||||
|
getMessageContentFilename, getMessageMediaHash,
|
||||||
|
} from '../../modules/helpers';
|
||||||
|
import { pick } from '../../util/iteratees';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
activeDownloads: Record<number, number[]>;
|
||||||
|
messages: Record<number, {
|
||||||
|
byId: Record<number, ApiMessage>;
|
||||||
|
threadsById: Record<number, Thread>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DispatchProps = Pick<GlobalActions, 'cancelMessageMediaDownload'>;
|
||||||
|
|
||||||
|
const startedDownloads = new Set<string>();
|
||||||
|
|
||||||
|
const DownloadsManager: FC<StateProps & DispatchProps> = ({
|
||||||
|
activeDownloads,
|
||||||
|
messages,
|
||||||
|
cancelMessageMediaDownload,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
Object.entries(activeDownloads).forEach(([chatId, messageIds]) => {
|
||||||
|
const activeMessages = messageIds.map((id) => messages[Number(chatId)].byId[id]);
|
||||||
|
activeMessages.forEach((message) => {
|
||||||
|
const downloadHash = getMessageMediaHash(message, 'download');
|
||||||
|
if (!downloadHash) {
|
||||||
|
cancelMessageMediaDownload({ message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startedDownloads.has(downloadHash)) {
|
||||||
|
const mediaData = mediaLoader.getFromMemory<ApiMediaFormat.BlobUrl>(downloadHash);
|
||||||
|
if (mediaData) {
|
||||||
|
startedDownloads.delete(downloadHash);
|
||||||
|
download(mediaData, getMessageContentFilename(message));
|
||||||
|
cancelMessageMediaDownload({ message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaLoader.fetch(downloadHash, ApiMediaFormat.BlobUrl, true).then((result) => {
|
||||||
|
startedDownloads.delete(downloadHash);
|
||||||
|
if (result) {
|
||||||
|
download(result, getMessageContentFilename(message));
|
||||||
|
}
|
||||||
|
cancelMessageMediaDownload({ message });
|
||||||
|
});
|
||||||
|
|
||||||
|
startedDownloads.add(downloadHash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
cancelMessageMediaDownload,
|
||||||
|
messages,
|
||||||
|
activeDownloads,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal(
|
||||||
|
(global): StateProps => {
|
||||||
|
const activeDownloads = global.activeDownloads.byChatId;
|
||||||
|
const messages = global.messages.byChatId;
|
||||||
|
return {
|
||||||
|
activeDownloads,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, ['cancelMessageMediaDownload']),
|
||||||
|
)(DownloadsManager));
|
||||||
@ -36,6 +36,7 @@ import MiddleColumn from '../middle/MiddleColumn';
|
|||||||
import RightColumn from '../right/RightColumn';
|
import RightColumn from '../right/RightColumn';
|
||||||
import MediaViewer from '../mediaViewer/MediaViewer.async';
|
import MediaViewer from '../mediaViewer/MediaViewer.async';
|
||||||
import AudioPlayer from '../middle/AudioPlayer';
|
import AudioPlayer from '../middle/AudioPlayer';
|
||||||
|
import DownloadManager from './DownloadManager';
|
||||||
import Notifications from './Notifications.async';
|
import Notifications from './Notifications.async';
|
||||||
import Dialogs from './Dialogs.async';
|
import Dialogs from './Dialogs.async';
|
||||||
import ForwardPicker from './ForwardPicker.async';
|
import ForwardPicker from './ForwardPicker.async';
|
||||||
@ -252,6 +253,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
onClose={handleStickerSetModalClose}
|
onClose={handleStickerSetModalClose}
|
||||||
stickerSetShortName={openedStickerSetShortName}
|
stickerSetShortName={openedStickerSetShortName}
|
||||||
/>
|
/>
|
||||||
|
<DownloadManager />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,7 +51,7 @@ import captureEscKeyListener from '../../util/captureEscKeyListener';
|
|||||||
import { stopCurrentAudio } from '../../util/audioPlayer';
|
import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||||
import useBlurSync from '../../hooks/useBlurSync';
|
import useBlurSync from '../../hooks/useBlurSync';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
@ -181,7 +181,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
|||||||
undefined,
|
undefined,
|
||||||
isGhostAnimation && ANIMATION_DURATION,
|
isGhostAnimation && ANIMATION_DURATION,
|
||||||
);
|
);
|
||||||
const { mediaData: fullMediaBlobUrl, downloadProgress } = useMediaWithDownloadProgress(
|
const { mediaData: fullMediaBlobUrl, loadProgress } = useMediaWithLoadProgress(
|
||||||
getMediaHash(true),
|
getMediaHash(true),
|
||||||
undefined,
|
undefined,
|
||||||
message && getMessageMediaFormat(message, 'viewerFull'),
|
message && getMessageMediaFormat(message, 'viewerFull'),
|
||||||
@ -485,7 +485,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
|||||||
isGif={isGif}
|
isGif={isGif}
|
||||||
posterData={bestImageData}
|
posterData={bestImageData}
|
||||||
posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)}
|
posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)}
|
||||||
downloadProgress={downloadProgress}
|
loadProgress={loadProgress}
|
||||||
fileSize={videoSize!}
|
fileSize={videoSize!}
|
||||||
isMediaViewerOpen={isOpen}
|
isMediaViewerOpen={isOpen}
|
||||||
noPlay={!isActive}
|
noPlay={!isActive}
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
import React, { FC, useMemo } from '../../lib/teact/teact';
|
import React, {
|
||||||
|
FC,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { GlobalActions } from '../../global/types';
|
||||||
import { ApiMessage } from '../../api/types';
|
import { ApiMessage } from '../../api/types';
|
||||||
|
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import { getMessageMediaHash } from '../../modules/helpers';
|
import { getMessageMediaHash } from '../../modules/helpers';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useMediaDownload from '../../hooks/useMediaDownload';
|
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||||
|
import { selectIsDownloading } from '../../modules/selectors';
|
||||||
|
import { pick } from '../../util/iteratees';
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import DropdownMenu from '../ui/DropdownMenu';
|
import DropdownMenu from '../ui/DropdownMenu';
|
||||||
@ -14,6 +23,10 @@ import ProgressSpinner from '../ui/ProgressSpinner';
|
|||||||
|
|
||||||
import './MediaViewerActions.scss';
|
import './MediaViewerActions.scss';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
isDownloading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
mediaData?: string;
|
mediaData?: string;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
@ -26,26 +39,35 @@ type OwnProps = {
|
|||||||
onZoomToggle: NoneToVoidFunction;
|
onZoomToggle: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MediaViewerActions: FC<OwnProps> = ({
|
type DispatchProps = Pick<GlobalActions, 'downloadMessageMedia' | 'cancelMessageMediaDownload'>;
|
||||||
|
|
||||||
|
const MediaViewerActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
mediaData,
|
mediaData,
|
||||||
isVideo,
|
isVideo,
|
||||||
isZoomed,
|
isZoomed,
|
||||||
message,
|
message,
|
||||||
fileName,
|
fileName,
|
||||||
isAvatar,
|
isAvatar,
|
||||||
|
isDownloading,
|
||||||
onCloseMediaViewer,
|
onCloseMediaViewer,
|
||||||
onForward,
|
onForward,
|
||||||
onZoomToggle,
|
onZoomToggle,
|
||||||
|
downloadMessageMedia,
|
||||||
|
cancelMessageMediaDownload,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
isDownloadStarted,
|
message && getMessageMediaHash(message, 'download'),
|
||||||
downloadProgress,
|
!isDownloading,
|
||||||
handleDownloadClick,
|
|
||||||
} = useMediaDownload(
|
|
||||||
message && isVideo ? getMessageMediaHash(message, 'download') : undefined,
|
|
||||||
fileName,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDownloadClick = useCallback(() => {
|
||||||
|
if (isDownloading) {
|
||||||
|
cancelMessageMediaDownload({ message });
|
||||||
|
} else {
|
||||||
|
downloadMessageMedia({ message });
|
||||||
|
}
|
||||||
|
}, [cancelMessageMediaDownload, downloadMessageMedia, isDownloading, message]);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const MenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
const MenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||||
@ -80,10 +102,10 @@ const MediaViewerActions: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
{isVideo ? (
|
{isVideo ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={isDownloadStarted ? 'close' : 'download'}
|
icon={isDownloading ? 'close' : 'download'}
|
||||||
onClick={handleDownloadClick}
|
onClick={handleDownloadClick}
|
||||||
>
|
>
|
||||||
{isDownloadStarted ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'}
|
{isDownloading ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -95,7 +117,7 @@ const MediaViewerActions: FC<OwnProps> = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{isDownloadStarted && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
{isDownloading && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -123,7 +145,7 @@ const MediaViewerActions: FC<OwnProps> = ({
|
|||||||
ariaLabel={lang('AccActionDownload')}
|
ariaLabel={lang('AccActionDownload')}
|
||||||
onClick={handleDownloadClick}
|
onClick={handleDownloadClick}
|
||||||
>
|
>
|
||||||
{isDownloadStarted ? (
|
{isDownloading ? (
|
||||||
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleDownloadClick} />
|
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleDownloadClick} />
|
||||||
) : (
|
) : (
|
||||||
<i className="icon-download" />
|
<i className="icon-download" />
|
||||||
@ -163,4 +185,16 @@ const MediaViewerActions: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MediaViewerActions;
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { message }): StateProps => {
|
||||||
|
const isDownloading = message ? selectIsDownloading(global, message) : false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDownloading,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
'downloadMessageMedia',
|
||||||
|
'cancelMessageMediaDownload',
|
||||||
|
]),
|
||||||
|
)(MediaViewerActions));
|
||||||
|
|||||||
@ -22,7 +22,7 @@ type OwnProps = {
|
|||||||
isGif?: boolean;
|
isGif?: boolean;
|
||||||
posterData?: string;
|
posterData?: string;
|
||||||
posterSize?: ApiDimensions;
|
posterSize?: ApiDimensions;
|
||||||
downloadProgress?: number;
|
loadProgress?: number;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
isMediaViewerOpen?: boolean;
|
isMediaViewerOpen?: boolean;
|
||||||
noPlay?: boolean;
|
noPlay?: boolean;
|
||||||
@ -36,7 +36,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
isGif,
|
isGif,
|
||||||
posterData,
|
posterData,
|
||||||
posterSize,
|
posterSize,
|
||||||
downloadProgress,
|
loadProgress,
|
||||||
fileSize,
|
fileSize,
|
||||||
isMediaViewerOpen,
|
isMediaViewerOpen,
|
||||||
noPlay,
|
noPlay,
|
||||||
@ -196,7 +196,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
{!isBuffered && <div className="buffering">Buffering...</div>}
|
{!isBuffered && <div className="buffering">Buffering...</div>}
|
||||||
<ProgressSpinner
|
<ProgressSpinner
|
||||||
size="xl"
|
size="xl"
|
||||||
progress={isBuffered ? 1 : downloadProgress}
|
progress={isBuffered ? 1 : loadProgress}
|
||||||
square
|
square
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -159,10 +159,10 @@ function renderTime(currentTime: number, duration: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFileSize(downloadedPercent: number, totalSize: number) {
|
function renderFileSize(loadedPercent: number, totalSize: number) {
|
||||||
return (
|
return (
|
||||||
<div className="player-file-size">
|
<div className="player-file-size">
|
||||||
{`${formatFileSize(totalSize * downloadedPercent)} / ${formatFileSize(totalSize)}`}
|
{`${formatFileSize(totalSize * loadedPercent)} / ${formatFileSize(totalSize)}`}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
||||||
bottom: calc(.5rem + env(safe-area-inset-bottom));
|
bottom: calc(0.5rem + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.mask-image-disabled &::before {
|
.mask-image-disabled &::before {
|
||||||
@ -123,16 +123,53 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.MenuItem {
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--color-text);
|
||||||
|
--ripple-color: rgba(0, 0, 0, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
padding: 0.6875rem;
|
padding: 0.6875rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
margin-right: 0;
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-text {
|
&.destructive {
|
||||||
display: none;
|
color: var(--color-error);
|
||||||
|
i {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.disabled):active {
|
||||||
|
background-color: var(--color-item-active);
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover, &:focus {
|
||||||
|
background-color: var(--color-chat-hover);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import React, { FC, memo, useEffect } from '../../lib/teact/teact';
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../lib/teact/teactn';
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { GlobalActions, MessageListType } from '../../global/types';
|
import { GlobalActions, MessageListType } from '../../global/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectCanDeleteSelectedMessages,
|
selectCanDeleteSelectedMessages,
|
||||||
|
selectCanDownloadSelectedMessages,
|
||||||
selectCanReportSelectedMessages,
|
selectCanReportSelectedMessages,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectSelectedMessagesCount,
|
selectSelectedMessagesCount,
|
||||||
@ -17,7 +20,6 @@ import usePrevious from '../../hooks/usePrevious';
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import MenuItem from '../ui/MenuItem';
|
|
||||||
|
|
||||||
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
|
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
|
||||||
import ReportMessageModal from '../common/ReportMessageModal';
|
import ReportMessageModal from '../common/ReportMessageModal';
|
||||||
@ -35,10 +37,13 @@ type StateProps = {
|
|||||||
selectedMessagesCount?: number;
|
selectedMessagesCount?: number;
|
||||||
canDeleteMessages?: boolean;
|
canDeleteMessages?: boolean;
|
||||||
canReportMessages?: boolean;
|
canReportMessages?: boolean;
|
||||||
|
canDownloadMessages?: boolean;
|
||||||
selectedMessageIds?: number[];
|
selectedMessageIds?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'exitMessageSelectMode' | 'openForwardMenuForSelectedMessages'>;
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
|
'exitMessageSelectMode' | 'openForwardMenuForSelectedMessages' | 'downloadSelectedMessages'
|
||||||
|
)>;
|
||||||
|
|
||||||
const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
canPost,
|
canPost,
|
||||||
@ -48,9 +53,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
selectedMessagesCount,
|
selectedMessagesCount,
|
||||||
canDeleteMessages,
|
canDeleteMessages,
|
||||||
canReportMessages,
|
canReportMessages,
|
||||||
|
canDownloadMessages,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
exitMessageSelectMode,
|
exitMessageSelectMode,
|
||||||
openForwardMenuForSelectedMessages,
|
openForwardMenuForSelectedMessages,
|
||||||
|
downloadSelectedMessages,
|
||||||
}) => {
|
}) => {
|
||||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||||
@ -65,6 +72,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
}, [isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode]);
|
}, [isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode]);
|
||||||
|
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
downloadSelectedMessages();
|
||||||
|
exitMessageSelectMode();
|
||||||
|
}, [downloadSelectedMessages, exitMessageSelectMode]);
|
||||||
|
|
||||||
const prevSelectedMessagesCount = usePrevious(selectedMessagesCount || undefined, true);
|
const prevSelectedMessagesCount = usePrevious(selectedMessagesCount || undefined, true);
|
||||||
const renderingSelectedMessagesCount = isActive ? selectedMessagesCount : prevSelectedMessagesCount;
|
const renderingSelectedMessagesCount = isActive ? selectedMessagesCount : prevSelectedMessagesCount;
|
||||||
|
|
||||||
@ -78,6 +90,26 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isActive && 'shown',
|
isActive && 'shown',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderButton = (
|
||||||
|
icon: string, label: string, onClick: AnyToVoidFunction, disabled?: boolean, destructive?: boolean,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={buildClassName(
|
||||||
|
'item',
|
||||||
|
disabled && 'disabled',
|
||||||
|
destructive && 'destructive',
|
||||||
|
)}
|
||||||
|
onClick={!disabled ? onClick : undefined}
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
<i className={`icon-${icon}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="MessageSelectToolbar-inner">
|
<div className="MessageSelectToolbar-inner">
|
||||||
@ -96,39 +128,15 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{!!selectedMessagesCount && (
|
{!!selectedMessagesCount && (
|
||||||
<div className="MessageSelectToolbar-actions">
|
<div className="MessageSelectToolbar-actions">
|
||||||
{messageListType !== 'scheduled' && (
|
{messageListType !== 'scheduled' && (
|
||||||
<MenuItem
|
renderButton('forward', lang('Chat.ForwardActionHeader'), openForwardMenuForSelectedMessages)
|
||||||
icon="forward"
|
|
||||||
ariaLabel="Forward Messages"
|
|
||||||
onClick={openForwardMenuForSelectedMessages}
|
|
||||||
>
|
|
||||||
<span className="item-text">
|
|
||||||
{lang('Forward')}
|
|
||||||
</span>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
{canReportMessages && (
|
{canReportMessages && (
|
||||||
<MenuItem
|
renderButton('flag', lang('Conversation.ReportMessages'), openReportModal)
|
||||||
icon="flag"
|
|
||||||
onClick={openReportModal}
|
|
||||||
disabled={!canReportMessages}
|
|
||||||
ariaLabel={lang('Conversation.ReportMessages')}
|
|
||||||
>
|
|
||||||
<span className="item-text">
|
|
||||||
{lang('Report')}
|
|
||||||
</span>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
{canDownloadMessages && (
|
||||||
destructive
|
renderButton('download', lang('lng_media_download'), handleDownload)
|
||||||
icon="delete"
|
)}
|
||||||
onClick={openDeleteModal}
|
{renderButton('delete', lang('EditAdminGroupDeleteMessages'), openDeleteModal, !canDeleteMessages, true)}
|
||||||
disabled={!canDeleteMessages}
|
|
||||||
ariaLabel={lang('EditAdminGroupDeleteMessages')}
|
|
||||||
>
|
|
||||||
<span className="item-text">
|
|
||||||
{lang('Delete')}
|
|
||||||
</span>
|
|
||||||
</MenuItem>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -151,6 +159,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const { type: messageListType } = selectCurrentMessageList(global) || {};
|
const { type: messageListType } = selectCurrentMessageList(global) || {};
|
||||||
const { canDelete } = selectCanDeleteSelectedMessages(global);
|
const { canDelete } = selectCanDeleteSelectedMessages(global);
|
||||||
const canReport = selectCanReportSelectedMessages(global);
|
const canReport = selectCanReportSelectedMessages(global);
|
||||||
|
const canDownload = selectCanDownloadSelectedMessages(global);
|
||||||
const { messageIds: selectedMessageIds } = global.selectedMessages || {};
|
const { messageIds: selectedMessageIds } = global.selectedMessages || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -158,8 +167,11 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
selectedMessagesCount: selectSelectedMessagesCount(global),
|
selectedMessagesCount: selectSelectedMessagesCount(global),
|
||||||
canDeleteMessages: canDelete,
|
canDeleteMessages: canDelete,
|
||||||
canReportMessages: canReport,
|
canReportMessages: canReport,
|
||||||
|
canDownloadMessages: canDownload,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['exitMessageSelectMode', 'openForwardMenuForSelectedMessages']),
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
'exitMessageSelectMode', 'openForwardMenuForSelectedMessages', 'downloadSelectedMessages',
|
||||||
|
]),
|
||||||
)(MessageSelectToolbar));
|
)(MessageSelectToolbar));
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { withGlobal } from '../../../lib/teact/teactn';
|
|||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import withSelectControl from './hocs/withSelectControl';
|
import withSelectControl from './hocs/withSelectControl';
|
||||||
import { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import { selectTheme } from '../../../modules/selectors';
|
import { selectActiveDownloadIds, selectTheme } from '../../../modules/selectors';
|
||||||
|
|
||||||
import Photo from './Photo';
|
import Photo from './Photo';
|
||||||
import Video from './Video';
|
import Video from './Video';
|
||||||
@ -35,6 +35,7 @@ type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
uploadsById: GlobalState['fileUploads']['byMessageLocalId'];
|
uploadsById: GlobalState['fileUploads']['byMessageLocalId'];
|
||||||
|
activeDownloadIds: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'cancelSendingMessage'>;
|
type DispatchProps = Pick<GlobalActions, 'cancelSendingMessage'>;
|
||||||
@ -50,6 +51,7 @@ const Album: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
albumLayout,
|
albumLayout,
|
||||||
onMediaClick,
|
onMediaClick,
|
||||||
uploadsById,
|
uploadsById,
|
||||||
|
activeDownloadIds,
|
||||||
theme,
|
theme,
|
||||||
cancelSendingMessage,
|
cancelSendingMessage,
|
||||||
}) => {
|
}) => {
|
||||||
@ -82,6 +84,7 @@ const Album: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
dimensions={dimensions}
|
dimensions={dimensions}
|
||||||
onClick={onMediaClick}
|
onClick={onMediaClick}
|
||||||
onCancelUpload={handleCancelUpload}
|
onCancelUpload={handleCancelUpload}
|
||||||
|
isDownloading={activeDownloadIds.includes(message.id)}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -98,6 +101,7 @@ const Album: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
dimensions={dimensions}
|
dimensions={dimensions}
|
||||||
onClick={onMediaClick}
|
onClick={onMediaClick}
|
||||||
onCancelUpload={handleCancelUpload}
|
onCancelUpload={handleCancelUpload}
|
||||||
|
isDownloading={activeDownloadIds.includes(message.id)}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -120,11 +124,14 @@ const Album: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default withGlobal<OwnProps>(
|
export default withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global, { album }): StateProps => {
|
||||||
|
const { chatId } = album.mainMessage;
|
||||||
const theme = selectTheme(global);
|
const theme = selectTheme(global);
|
||||||
|
const activeDownloadIds = selectActiveDownloadIds(global, chatId);
|
||||||
return {
|
return {
|
||||||
theme,
|
theme,
|
||||||
uploadsById: global.fileUploads.byMessageLocalId,
|
uploadsById: global.fileUploads.byMessageLocalId,
|
||||||
|
activeDownloadIds,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
|||||||
@ -6,7 +6,11 @@ import { withGlobal } from '../../../lib/teact/teactn';
|
|||||||
import { GlobalActions, MessageListType } from '../../../global/types';
|
import { GlobalActions, MessageListType } from '../../../global/types';
|
||||||
import { ApiMessage } from '../../../api/types';
|
import { ApiMessage } from '../../../api/types';
|
||||||
import { IAlbum, IAnchorPosition } from '../../../types';
|
import { IAlbum, IAnchorPosition } from '../../../types';
|
||||||
import { selectAllowedMessageActions, selectCurrentMessageList } from '../../../modules/selectors';
|
import {
|
||||||
|
selectActiveDownloadIds,
|
||||||
|
selectAllowedMessageActions,
|
||||||
|
selectCurrentMessageList,
|
||||||
|
} from '../../../modules/selectors';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
@ -46,11 +50,14 @@ type StateProps = {
|
|||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
canCopyLink?: boolean;
|
canCopyLink?: boolean;
|
||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
|
canDownload?: boolean;
|
||||||
|
activeDownloads: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'setReplyingToId' | 'setEditingId' | 'pinMessage' | 'openForwardMenu' |
|
'setReplyingToId' | 'setEditingId' | 'pinMessage' | 'openForwardMenu' |
|
||||||
'faveSticker' | 'unfaveSticker' | 'toggleMessageSelection' | 'sendScheduledMessages' | 'rescheduleMessage'
|
'faveSticker' | 'unfaveSticker' | 'toggleMessageSelection' | 'sendScheduledMessages' | 'rescheduleMessage' |
|
||||||
|
'downloadMessageMedia' | 'cancelMessageMediaDownload'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
@ -77,6 +84,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canCopy,
|
canCopy,
|
||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
|
canDownload,
|
||||||
|
activeDownloads,
|
||||||
setReplyingToId,
|
setReplyingToId,
|
||||||
setEditingId,
|
setEditingId,
|
||||||
pinMessage,
|
pinMessage,
|
||||||
@ -86,6 +95,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
toggleMessageSelection,
|
toggleMessageSelection,
|
||||||
sendScheduledMessages,
|
sendScheduledMessages,
|
||||||
rescheduleMessage,
|
rescheduleMessage,
|
||||||
|
downloadMessageMedia,
|
||||||
|
cancelMessageMediaDownload,
|
||||||
}) => {
|
}) => {
|
||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
@ -94,6 +105,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
||||||
|
|
||||||
|
const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id))
|
||||||
|
: activeDownloads.includes(message.id);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -205,6 +219,17 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [chatUsername, closeMenu, message.chatId, message.id]);
|
}, [chatUsername, closeMenu, message.chatId, message.id]);
|
||||||
|
|
||||||
|
const handleDownloadClick = useCallback(() => {
|
||||||
|
(album?.messages || [message]).forEach((msg) => {
|
||||||
|
if (isDownloading) {
|
||||||
|
cancelMessageMediaDownload({ message: msg });
|
||||||
|
} else {
|
||||||
|
downloadMessageMedia({ message: msg });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
closeMenu();
|
||||||
|
}, [album, message, closeMenu, isDownloading, cancelMessageMediaDownload, downloadMessageMedia]);
|
||||||
|
|
||||||
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
||||||
|
|
||||||
if (noOptions) {
|
if (noOptions) {
|
||||||
@ -236,6 +261,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canCopy={canCopy}
|
canCopy={canCopy}
|
||||||
canCopyLink={canCopyLink}
|
canCopyLink={canCopyLink}
|
||||||
canSelect={canSelect}
|
canSelect={canSelect}
|
||||||
|
canDownload={canDownload}
|
||||||
|
isDownloading={isDownloading}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onPin={handlePin}
|
onPin={handlePin}
|
||||||
@ -250,6 +277,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onReschedule={handleOpenCalendar}
|
onReschedule={handleOpenCalendar}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
onCopyLink={handleCopyLink}
|
onCopyLink={handleCopyLink}
|
||||||
|
onDownload={handleDownloadClick}
|
||||||
/>
|
/>
|
||||||
<DeleteMessageModal
|
<DeleteMessageModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
@ -285,6 +313,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { message, messageListType }): StateProps => {
|
(global, { message, messageListType }): StateProps => {
|
||||||
const { threadId } = selectCurrentMessageList(global) || {};
|
const { threadId } = selectCurrentMessageList(global) || {};
|
||||||
|
const activeDownloads = selectActiveDownloadIds(global, message.chatId);
|
||||||
const {
|
const {
|
||||||
noOptions,
|
noOptions,
|
||||||
canReply,
|
canReply,
|
||||||
@ -299,6 +328,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canCopy,
|
canCopy,
|
||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
|
canDownload,
|
||||||
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
const isPinned = messageListType === 'pinned';
|
const isPinned = messageListType === 'pinned';
|
||||||
const isScheduled = messageListType === 'scheduled';
|
const isScheduled = messageListType === 'scheduled';
|
||||||
@ -319,6 +349,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canCopy,
|
canCopy,
|
||||||
canCopyLink: !isScheduled && canCopyLink,
|
canCopyLink: !isScheduled && canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
|
canDownload,
|
||||||
|
activeDownloads,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
@ -331,5 +363,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'toggleMessageSelection',
|
'toggleMessageSelection',
|
||||||
'sendScheduledMessages',
|
'sendScheduledMessages',
|
||||||
'rescheduleMessage',
|
'rescheduleMessage',
|
||||||
|
'downloadMessageMedia',
|
||||||
|
'cancelMessageMediaDownload',
|
||||||
]),
|
]),
|
||||||
)(ContextMenuContainer));
|
)(ContextMenuContainer));
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import {
|
|||||||
selectShouldLoopStickers,
|
selectShouldLoopStickers,
|
||||||
selectTheme,
|
selectTheme,
|
||||||
selectAllowedMessageActions,
|
selectAllowedMessageActions,
|
||||||
|
selectIsDownloading,
|
||||||
} from '../../../modules/selectors';
|
} from '../../../modules/selectors';
|
||||||
import {
|
import {
|
||||||
getMessageContent,
|
getMessageContent,
|
||||||
@ -153,6 +154,7 @@ type StateProps = {
|
|||||||
isInSelectMode?: boolean;
|
isInSelectMode?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isGroupSelected?: boolean;
|
isGroupSelected?: boolean;
|
||||||
|
isDownloading: boolean;
|
||||||
threadId?: number;
|
threadId?: number;
|
||||||
isPinnedList?: boolean;
|
isPinnedList?: boolean;
|
||||||
shouldAutoLoadMedia?: boolean;
|
shouldAutoLoadMedia?: boolean;
|
||||||
@ -217,6 +219,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
threadId,
|
threadId,
|
||||||
messageListType,
|
messageListType,
|
||||||
isPinnedList,
|
isPinnedList,
|
||||||
|
isDownloading,
|
||||||
shouldAutoLoadMedia,
|
shouldAutoLoadMedia,
|
||||||
shouldAutoPlayMedia,
|
shouldAutoPlayMedia,
|
||||||
shouldLoopStickers,
|
shouldLoopStickers,
|
||||||
@ -533,6 +536,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
shouldAffectAppendix={hasCustomAppendix}
|
shouldAffectAppendix={hasCustomAppendix}
|
||||||
onClick={handleMediaClick}
|
onClick={handleMediaClick}
|
||||||
onCancelUpload={handleCancelUpload}
|
onCancelUpload={handleCancelUpload}
|
||||||
|
isDownloading={isDownloading}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -543,6 +547,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
shouldAutoLoad={shouldAutoLoadMedia}
|
shouldAutoLoad={shouldAutoLoadMedia}
|
||||||
shouldAutoPlay={shouldAutoPlayMedia}
|
shouldAutoPlay={shouldAutoPlayMedia}
|
||||||
lastSyncTime={lastSyncTime}
|
lastSyncTime={lastSyncTime}
|
||||||
|
isDownloading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isAlbum && video && !video.isRound && (
|
{!isAlbum && video && !video.isRound && (
|
||||||
@ -556,6 +561,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
lastSyncTime={lastSyncTime}
|
lastSyncTime={lastSyncTime}
|
||||||
onClick={handleMediaClick}
|
onClick={handleMediaClick}
|
||||||
onCancelUpload={handleCancelUpload}
|
onCancelUpload={handleCancelUpload}
|
||||||
|
isDownloading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(audio || voice) && (
|
{(audio || voice) && (
|
||||||
@ -570,6 +576,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onPlay={handleAudioPlay}
|
onPlay={handleAudioPlay}
|
||||||
onReadMedia={voice && (!isOwn || isChatWithSelf) ? handleReadMedia : undefined}
|
onReadMedia={voice && (!isOwn || isChatWithSelf) ? handleReadMedia : undefined}
|
||||||
onCancelUpload={handleCancelUpload}
|
onCancelUpload={handleCancelUpload}
|
||||||
|
isDownloading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{document && (
|
{document && (
|
||||||
@ -581,6 +588,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onMediaClick={handleMediaClick}
|
onMediaClick={handleMediaClick}
|
||||||
onCancelUpload={handleCancelUpload}
|
onCancelUpload={handleCancelUpload}
|
||||||
|
isDownloading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{contact && (
|
{contact && (
|
||||||
@ -612,6 +620,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
lastSyncTime={lastSyncTime}
|
lastSyncTime={lastSyncTime}
|
||||||
onMediaClick={handleMediaClick}
|
onMediaClick={handleMediaClick}
|
||||||
onCancelMediaTransfer={handleCancelUpload}
|
onCancelMediaTransfer={handleCancelUpload}
|
||||||
|
isDownloading={isDownloading}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -859,6 +868,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { canReply } = (messageListType === 'thread' && selectAllowedMessageActions(global, message, threadId)) || {};
|
const { canReply } = (messageListType === 'thread' && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
|
const isDownloading = selectIsDownloading(global, message);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
@ -887,6 +897,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
!!message.groupedId && !message.isInAlbum && selectIsDocumentGroupSelected(global, chatId, message.groupedId)
|
!!message.groupedId && !message.isInAlbum && selectIsDocumentGroupSelected(global, chatId, message.groupedId)
|
||||||
),
|
),
|
||||||
threadId,
|
threadId,
|
||||||
|
isDownloading,
|
||||||
isPinnedList: messageListType === 'pinned',
|
isPinnedList: messageListType === 'pinned',
|
||||||
shouldAutoLoadMedia: chat ? selectShouldAutoLoadMedia(global, message, chat, sender) : undefined,
|
shouldAutoLoadMedia: chat ? selectShouldAutoLoadMedia(global, message, chat, sender) : undefined,
|
||||||
shouldAutoPlayMedia: selectShouldAutoPlayMedia(global, message),
|
shouldAutoPlayMedia: selectShouldAutoPlayMedia(global, message),
|
||||||
|
|||||||
@ -33,6 +33,8 @@ type OwnProps = {
|
|||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
canCopyLink?: boolean;
|
canCopyLink?: boolean;
|
||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
|
canDownload?: boolean;
|
||||||
|
isDownloading?: boolean;
|
||||||
onReply: () => void;
|
onReply: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onPin: () => void;
|
onPin: () => void;
|
||||||
@ -48,6 +50,7 @@ type OwnProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: () => void;
|
||||||
onCopyLink?: () => void;
|
onCopyLink?: () => void;
|
||||||
|
onDownload?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCROLLBAR_WIDTH = 10;
|
const SCROLLBAR_WIDTH = 10;
|
||||||
@ -70,6 +73,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canCopy,
|
canCopy,
|
||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
|
canDownload,
|
||||||
|
isDownloading,
|
||||||
onReply,
|
onReply,
|
||||||
onEdit,
|
onEdit,
|
||||||
onPin,
|
onPin,
|
||||||
@ -85,6 +90,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
onCopyLink,
|
onCopyLink,
|
||||||
|
onDownload,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -152,6 +158,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
))}
|
))}
|
||||||
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
||||||
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
||||||
|
{canDownload && (
|
||||||
|
<MenuItem icon="download" onClick={onDownload}>
|
||||||
|
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
||||||
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
||||||
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
isOwnMessage,
|
isOwnMessage,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||||
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
||||||
@ -38,6 +38,7 @@ export type OwnProps = {
|
|||||||
shouldAffectAppendix?: boolean;
|
shouldAffectAppendix?: boolean;
|
||||||
dimensions?: IMediaDimensions & { isSmall?: boolean };
|
dimensions?: IMediaDimensions & { isSmall?: boolean };
|
||||||
nonInteractive?: boolean;
|
nonInteractive?: boolean;
|
||||||
|
isDownloading: boolean;
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
onClick?: (id: number) => void;
|
onClick?: (id: number) => void;
|
||||||
onCancelUpload?: (message: ApiMessage) => void;
|
onCancelUpload?: (message: ApiMessage) => void;
|
||||||
@ -58,6 +59,7 @@ const Photo: FC<OwnProps> = ({
|
|||||||
dimensions,
|
dimensions,
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
shouldAffectAppendix,
|
shouldAffectAppendix,
|
||||||
|
isDownloading,
|
||||||
theme,
|
theme,
|
||||||
onClick,
|
onClick,
|
||||||
onCancelUpload,
|
onCancelUpload,
|
||||||
@ -70,22 +72,30 @@ const Photo: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const [isDownloadAllowed, setIsDownloadAllowed] = useState(shouldAutoLoad);
|
const [isLoadAllowed, setIsLoadAllowed] = useState(shouldAutoLoad);
|
||||||
const shouldDownload = isDownloadAllowed && isIntersecting;
|
const shouldLoad = isLoadAllowed && isIntersecting;
|
||||||
const {
|
const {
|
||||||
mediaData, downloadProgress,
|
mediaData, loadProgress,
|
||||||
} = useMediaWithDownloadProgress(getMessageMediaHash(message, size), !shouldDownload);
|
} = useMediaWithLoadProgress(getMessageMediaHash(message, size), !shouldLoad);
|
||||||
const fullMediaData = localBlobUrl || mediaData;
|
const fullMediaData = localBlobUrl || mediaData;
|
||||||
const thumbRef = useBlurredMediaThumbRef(message, fullMediaData);
|
const thumbRef = useBlurredMediaThumbRef(message, fullMediaData);
|
||||||
|
|
||||||
|
const {
|
||||||
|
loadProgress: downloadProgress,
|
||||||
|
} = useMediaWithLoadProgress(getMessageMediaHash(message, 'download'), !isDownloading);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isUploading, isTransferring, transferProgress,
|
isUploading, isTransferring, transferProgress,
|
||||||
} = getMediaTransferState(message, uploadProgress || downloadProgress, shouldDownload && !fullMediaData);
|
} = getMediaTransferState(
|
||||||
const wasDownloadDisabled = usePrevious(isDownloadAllowed) === false;
|
message,
|
||||||
|
uploadProgress || isDownloading ? downloadProgress : loadProgress,
|
||||||
|
shouldLoad && !fullMediaData,
|
||||||
|
);
|
||||||
|
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderSpinner,
|
shouldRender: shouldRenderSpinner,
|
||||||
transitionClassNames: spinnerClassNames,
|
transitionClassNames: spinnerClassNames,
|
||||||
} = useShowTransition(isTransferring, undefined, wasDownloadDisabled, 'slow');
|
} = useShowTransition(isTransferring, undefined, wasLoadDisabled, 'slow');
|
||||||
const {
|
const {
|
||||||
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
|
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
|
||||||
} = useTransitionForMedia(fullMediaData, 'slow');
|
} = useTransitionForMedia(fullMediaData, 'slow');
|
||||||
@ -96,7 +106,7 @@ const Photo: FC<OwnProps> = ({
|
|||||||
onCancelUpload(message);
|
onCancelUpload(message);
|
||||||
}
|
}
|
||||||
} else if (!fullMediaData) {
|
} else if (!fullMediaData) {
|
||||||
setIsDownloadAllowed((isAllowed) => !isAllowed);
|
setIsLoadAllowed((isAllowed) => !isAllowed);
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
onClick(message.id);
|
onClick(message.id);
|
||||||
}
|
}
|
||||||
@ -164,11 +174,11 @@ const Photo: FC<OwnProps> = ({
|
|||||||
<ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} />
|
<ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fullMediaData && !isDownloadAllowed && (
|
{!fullMediaData && !isLoadAllowed && (
|
||||||
<i className="icon-download" />
|
<i className="icon-download" />
|
||||||
)}
|
)}
|
||||||
{isTransferring && (
|
{isTransferring && (
|
||||||
<span className="message-upload-progress">{Math.round(transferProgress * 100)}%</span>
|
<span className="message-transfer-progress">{Math.round(transferProgress * 100)}%</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,14 +5,15 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiMessage } from '../../../api/types';
|
import { ApiMediaFormat, ApiMessage } from '../../../api/types';
|
||||||
|
|
||||||
import { ROUND_VIDEO_DIMENSIONS } from '../../common/helpers/mediaDimensions';
|
import { ROUND_VIDEO_DIMENSIONS } from '../../common/helpers/mediaDimensions';
|
||||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||||
import { getMessageMediaFormat, getMessageMediaHash } from '../../../modules/helpers';
|
import { getMessageMediaFormat, getMessageMediaHash } from '../../../modules/helpers';
|
||||||
import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
@ -36,6 +37,7 @@ type OwnProps = {
|
|||||||
shouldAutoLoad?: boolean;
|
shouldAutoLoad?: boolean;
|
||||||
shouldAutoPlay?: boolean;
|
shouldAutoPlay?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
|
isDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentOnRelease: NoneToVoidFunction;
|
let currentOnRelease: NoneToVoidFunction;
|
||||||
@ -56,6 +58,7 @@ const RoundVideo: FC<OwnProps> = ({
|
|||||||
shouldAutoLoad,
|
shouldAutoLoad,
|
||||||
shouldAutoPlay,
|
shouldAutoPlay,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
isDownloading,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@ -68,23 +71,30 @@ const RoundVideo: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const [isDownloadAllowed, setIsDownloadAllowed] = useState(shouldAutoLoad && shouldAutoPlay);
|
const [isLoadAllowed, setIsLoadAllowed] = useState(shouldAutoLoad && shouldAutoPlay);
|
||||||
const shouldDownload = Boolean(isDownloadAllowed && isIntersecting && lastSyncTime);
|
const shouldLoad = Boolean(isLoadAllowed && isIntersecting && lastSyncTime);
|
||||||
const { mediaData, downloadProgress } = useMediaWithDownloadProgress(
|
const { mediaData, loadProgress } = useMediaWithLoadProgress(
|
||||||
getMessageMediaHash(message, 'inline'),
|
getMessageMediaHash(message, 'inline'),
|
||||||
!shouldDownload,
|
!shouldLoad,
|
||||||
getMessageMediaFormat(message, 'inline'),
|
getMessageMediaFormat(message, 'inline'),
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
|
getMessageMediaHash(message, 'download'),
|
||||||
|
!isDownloading,
|
||||||
|
ApiMediaFormat.BlobUrl,
|
||||||
|
lastSyncTime,
|
||||||
|
);
|
||||||
const thumbRef = useBlurredMediaThumbRef(message, mediaData);
|
const thumbRef = useBlurredMediaThumbRef(message, mediaData);
|
||||||
|
|
||||||
const { isBuffered, bufferingHandlers } = useBuffering();
|
const { isBuffered, bufferingHandlers } = useBuffering();
|
||||||
const isTransferring = isDownloadAllowed && !isBuffered;
|
const isTransferring = (isLoadAllowed && !isBuffered) || isDownloading;
|
||||||
const wasDownloadDisabled = usePrevious(isDownloadAllowed) === false;
|
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldSpinnerRender,
|
shouldRender: shouldSpinnerRender,
|
||||||
transitionClassNames: spinnerClassNames,
|
transitionClassNames: spinnerClassNames,
|
||||||
} = useShowTransition(isTransferring || !isBuffered, undefined, wasDownloadDisabled);
|
} = useShowTransition(isTransferring || !isBuffered, undefined, wasLoadDisabled);
|
||||||
const { shouldRenderThumb, transitionClassNames } = useTransitionForMedia(mediaData, 'slow');
|
const { shouldRenderThumb, transitionClassNames } = useTransitionForMedia(mediaData, 'slow');
|
||||||
|
|
||||||
const [isActivated, setIsActivated] = useState<boolean>(false);
|
const [isActivated, setIsActivated] = useState<boolean>(false);
|
||||||
@ -148,11 +158,16 @@ const RoundVideo: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (!mediaData) {
|
if (!mediaData) {
|
||||||
setIsDownloadAllowed((isAllowed) => !isAllowed);
|
setIsLoadAllowed((isAllowed) => !isAllowed);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDownloading) {
|
||||||
|
getDispatch().cancelMessageMediaDownload({ message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const playerEl = playerRef.current!;
|
const playerEl = playerRef.current!;
|
||||||
if (isActivated) {
|
if (isActivated) {
|
||||||
if (playerEl.paused) {
|
if (playerEl.paused) {
|
||||||
@ -171,7 +186,7 @@ const RoundVideo: FC<OwnProps> = ({
|
|||||||
|
|
||||||
setIsActivated(true);
|
setIsActivated(true);
|
||||||
}
|
}
|
||||||
}, [capturePlaying, isActivated, mediaData]);
|
}, [capturePlaying, isActivated, isDownloading, mediaData, message]);
|
||||||
|
|
||||||
const handleTimeUpdate = useCallback((e: React.UIEvent<HTMLVideoElement>) => {
|
const handleTimeUpdate = useCallback((e: React.UIEvent<HTMLVideoElement>) => {
|
||||||
const playerEl = e.currentTarget;
|
const playerEl = e.currentTarget;
|
||||||
@ -221,10 +236,10 @@ const RoundVideo: FC<OwnProps> = ({
|
|||||||
<div className="progress" ref={playingProgressRef} />
|
<div className="progress" ref={playingProgressRef} />
|
||||||
{shouldSpinnerRender && (
|
{shouldSpinnerRender && (
|
||||||
<div className={`media-loading ${spinnerClassNames}`}>
|
<div className={`media-loading ${spinnerClassNames}`}>
|
||||||
<ProgressSpinner progress={downloadProgress} />
|
<ProgressSpinner progress={isDownloading ? downloadProgress : loadProgress} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!mediaData && !isDownloadAllowed && (
|
{!mediaData && !isLoadAllowed && (
|
||||||
<i className="icon-large-play" />
|
<i className="icon-large-play" />
|
||||||
)}
|
)}
|
||||||
<div className="message-media-duration">
|
<div className="message-media-duration">
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useCallback, useRef, useState,
|
FC, useCallback, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiMessage } from '../../../api/types';
|
import { ApiMediaFormat, ApiMessage } from '../../../api/types';
|
||||||
import { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
import { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
||||||
|
|
||||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
isOwnMessage,
|
isOwnMessage,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress';
|
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
|
||||||
@ -41,6 +42,7 @@ export type OwnProps = {
|
|||||||
uploadProgress?: number;
|
uploadProgress?: number;
|
||||||
dimensions?: IMediaDimensions;
|
dimensions?: IMediaDimensions;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
|
isDownloading: boolean;
|
||||||
onClick?: (id: number) => void;
|
onClick?: (id: number) => void;
|
||||||
onCancelUpload?: (message: ApiMessage) => void;
|
onCancelUpload?: (message: ApiMessage) => void;
|
||||||
};
|
};
|
||||||
@ -57,6 +59,7 @@ const Video: FC<OwnProps> = ({
|
|||||||
dimensions,
|
dimensions,
|
||||||
onClick,
|
onClick,
|
||||||
onCancelUpload,
|
onCancelUpload,
|
||||||
|
isDownloading,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@ -68,8 +71,8 @@ const Video: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const [isDownloadAllowed, setIsDownloadAllowed] = useState(shouldAutoLoad);
|
const [isLoadAllowed, setIsLoadAllowed] = useState(shouldAutoLoad);
|
||||||
const shouldDownload = Boolean(isDownloadAllowed && isIntersecting && lastSyncTime);
|
const shouldLoad = Boolean(isLoadAllowed && isIntersecting && lastSyncTime);
|
||||||
const [isPlayAllowed, setIsPlayAllowed] = useState(shouldAutoPlay);
|
const [isPlayAllowed, setIsPlayAllowed] = useState(shouldAutoPlay);
|
||||||
|
|
||||||
const previewBlobUrl = useMedia(
|
const previewBlobUrl = useMedia(
|
||||||
@ -78,9 +81,9 @@ const Video: FC<OwnProps> = ({
|
|||||||
getMessageMediaFormat(message, 'pictogram'),
|
getMessageMediaFormat(message, 'pictogram'),
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
);
|
);
|
||||||
const { mediaData, downloadProgress } = useMediaWithDownloadProgress(
|
const { mediaData, loadProgress } = useMediaWithLoadProgress(
|
||||||
getMessageMediaHash(message, 'inline'),
|
getMessageMediaHash(message, 'inline'),
|
||||||
!shouldDownload,
|
!shouldLoad,
|
||||||
getMessageMediaFormat(message, 'inline'),
|
getMessageMediaFormat(message, 'inline'),
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
);
|
);
|
||||||
@ -89,17 +92,24 @@ const Video: FC<OwnProps> = ({
|
|||||||
// Thumbnail is always rendered so we can only disable blur if we have preview
|
// Thumbnail is always rendered so we can only disable blur if we have preview
|
||||||
const thumbRef = useBlurredMediaThumbRef(message, previewBlobUrl);
|
const thumbRef = useBlurredMediaThumbRef(message, previewBlobUrl);
|
||||||
|
|
||||||
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
|
getMessageMediaHash(message, 'download'),
|
||||||
|
!isDownloading,
|
||||||
|
ApiMediaFormat.BlobUrl,
|
||||||
|
lastSyncTime,
|
||||||
|
);
|
||||||
|
|
||||||
const { isBuffered, bufferingHandlers } = useBuffering(!shouldAutoLoad);
|
const { isBuffered, bufferingHandlers } = useBuffering(!shouldAutoLoad);
|
||||||
const { isUploading, isTransferring, transferProgress } = getMediaTransferState(
|
const { isUploading, isTransferring, transferProgress } = getMediaTransferState(
|
||||||
message,
|
message,
|
||||||
uploadProgress || downloadProgress,
|
uploadProgress || isDownloading ? downloadProgress : loadProgress,
|
||||||
shouldDownload && !isBuffered,
|
(shouldLoad && !isBuffered) || isDownloading,
|
||||||
);
|
);
|
||||||
const wasDownloadDisabled = usePrevious(isDownloadAllowed) === false;
|
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderSpinner,
|
shouldRender: shouldRenderSpinner,
|
||||||
transitionClassNames: spinnerClassNames,
|
transitionClassNames: spinnerClassNames,
|
||||||
} = useShowTransition(isTransferring, undefined, wasDownloadDisabled);
|
} = useShowTransition(isTransferring, undefined, wasLoadDisabled);
|
||||||
const { transitionClassNames } = useTransitionForMedia(fullMediaData, 'slow');
|
const { transitionClassNames } = useTransitionForMedia(fullMediaData, 'slow');
|
||||||
|
|
||||||
const [playProgress, setPlayProgress] = useState<number>(0);
|
const [playProgress, setPlayProgress] = useState<number>(0);
|
||||||
@ -122,15 +132,17 @@ const Video: FC<OwnProps> = ({
|
|||||||
if (onCancelUpload) {
|
if (onCancelUpload) {
|
||||||
onCancelUpload(message);
|
onCancelUpload(message);
|
||||||
}
|
}
|
||||||
|
} else if (isDownloading) {
|
||||||
|
getDispatch().cancelMessageMediaDownload({ message });
|
||||||
} else if (!fullMediaData) {
|
} else if (!fullMediaData) {
|
||||||
setIsDownloadAllowed((isAllowed) => !isAllowed);
|
setIsLoadAllowed((isAllowed) => !isAllowed);
|
||||||
} else if (fullMediaData && !isPlayAllowed) {
|
} else if (fullMediaData && !isPlayAllowed) {
|
||||||
setIsPlayAllowed(true);
|
setIsPlayAllowed(true);
|
||||||
videoRef.current!.play();
|
videoRef.current!.play();
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
onClick(message.id);
|
onClick(message.id);
|
||||||
}
|
}
|
||||||
}, [isUploading, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]);
|
}, [isUploading, isDownloading, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]);
|
||||||
|
|
||||||
const className = buildClassName('media-inner dark', !isUploading && 'interactive');
|
const className = buildClassName('media-inner dark', !isUploading && 'interactive');
|
||||||
const videoClassName = buildClassName('full-media', transitionClassNames);
|
const videoClassName = buildClassName('full-media', transitionClassNames);
|
||||||
@ -182,20 +194,20 @@ const Video: FC<OwnProps> = ({
|
|||||||
<source src={fullMediaData} />
|
<source src={fullMediaData} />
|
||||||
</video>
|
</video>
|
||||||
)}
|
)}
|
||||||
{(isDownloadAllowed && !isPlayAllowed && !shouldRenderSpinner) && (
|
{(isLoadAllowed && !isPlayAllowed && !shouldRenderSpinner) && (
|
||||||
<i className="icon-large-play" />
|
<i className="icon-large-play" />
|
||||||
)}
|
)}
|
||||||
{shouldRenderSpinner && (
|
{shouldRenderSpinner && (
|
||||||
<div className={`media-loading ${spinnerClassNames}`}>
|
<div className={`media-loading ${spinnerClassNames}`}>
|
||||||
<ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} />
|
<ProgressSpinner progress={transferProgress} onClick={handleClick} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isDownloadAllowed && (
|
{!isLoadAllowed && (
|
||||||
<i className="icon-download" />
|
<i className="icon-download" />
|
||||||
)}
|
)}
|
||||||
{isTransferring ? (
|
{isTransferring ? (
|
||||||
<span className="message-upload-progress">
|
<span className="message-transfer-progress">
|
||||||
{isUploading ? `${Math.round(transferProgress * 100)}%` : '...'}
|
{(isUploading || isDownloading) ? `${Math.round(transferProgress * 100)}%` : '...'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="message-media-duration">
|
<div className="message-media-duration">
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type OwnProps = {
|
|||||||
shouldAutoPlay?: boolean;
|
shouldAutoPlay?: boolean;
|
||||||
inPreview?: boolean;
|
inPreview?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
|
isDownloading?: boolean;
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
onMediaClick?: () => void;
|
onMediaClick?: () => void;
|
||||||
onCancelMediaTransfer?: () => void;
|
onCancelMediaTransfer?: () => void;
|
||||||
@ -39,6 +40,7 @@ const WebPage: FC<OwnProps> = ({
|
|||||||
shouldAutoPlay,
|
shouldAutoPlay,
|
||||||
inPreview,
|
inPreview,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
isDownloading = false,
|
||||||
theme,
|
theme,
|
||||||
onMediaClick,
|
onMediaClick,
|
||||||
onCancelMediaTransfer,
|
onCancelMediaTransfer,
|
||||||
@ -94,6 +96,7 @@ const WebPage: FC<OwnProps> = ({
|
|||||||
nonInteractive={!isMediaInteractive}
|
nonInteractive={!isMediaInteractive}
|
||||||
onClick={isMediaInteractive ? handleMediaClick : undefined}
|
onClick={isMediaInteractive ? handleMediaClick : undefined}
|
||||||
onCancelUpload={onCancelMediaTransfer}
|
onCancelUpload={onCancelMediaTransfer}
|
||||||
|
isDownloading={isDownloading}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -116,6 +119,7 @@ const WebPage: FC<OwnProps> = ({
|
|||||||
lastSyncTime={lastSyncTime}
|
lastSyncTime={lastSyncTime}
|
||||||
onClick={isMediaInteractive ? handleMediaClick : undefined}
|
onClick={isMediaInteractive ? handleMediaClick : undefined}
|
||||||
onCancelUpload={onCancelMediaTransfer}
|
onCancelUpload={onCancelMediaTransfer}
|
||||||
|
isDownloading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -490,7 +490,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-media-duration,
|
.message-media-duration,
|
||||||
.message-upload-progress {
|
.message-transfer-progress {
|
||||||
background: rgba(0, 0, 0, .25);
|
background: rgba(0, 0, 0, .25);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|||||||
@ -100,7 +100,7 @@
|
|||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
|
|
||||||
.ProgressSpinner,
|
.ProgressSpinner,
|
||||||
.message-upload-progress {
|
.message-transfer-progress {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
selectCurrentMediaSearch,
|
selectCurrentMediaSearch,
|
||||||
selectIsRightColumnShown,
|
selectIsRightColumnShown,
|
||||||
selectTheme,
|
selectTheme,
|
||||||
|
selectActiveDownloadIds,
|
||||||
} from '../../modules/selectors';
|
} from '../../modules/selectors';
|
||||||
import { pick } from '../../util/iteratees';
|
import { pick } from '../../util/iteratees';
|
||||||
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||||
@ -85,6 +86,7 @@ type StateProps = {
|
|||||||
isRestricted?: boolean;
|
isRestricted?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
serverTimeOffset: number;
|
serverTimeOffset: number;
|
||||||
|
activeDownloadIds: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
@ -123,6 +125,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isRightColumnShown,
|
isRightColumnShown,
|
||||||
isRestricted,
|
isRestricted,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
activeDownloadIds,
|
||||||
setLocalMediaSearchType,
|
setLocalMediaSearchType,
|
||||||
loadMoreMembers,
|
loadMoreMembers,
|
||||||
searchMediaMessagesLocal,
|
searchMediaMessagesLocal,
|
||||||
@ -316,6 +319,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
smaller
|
smaller
|
||||||
className="scroll-item"
|
className="scroll-item"
|
||||||
onDateClick={handleMessageFocus}
|
onDateClick={handleMessageFocus}
|
||||||
|
isDownloading={activeDownloadIds.includes(id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'links' ? (
|
) : resultType === 'links' ? (
|
||||||
@ -338,6 +342,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
className="scroll-item"
|
className="scroll-item"
|
||||||
onPlay={handlePlayAudio}
|
onPlay={handlePlayAudio}
|
||||||
onDateClick={handleMessageFocus}
|
onDateClick={handleMessageFocus}
|
||||||
|
isDownloading={activeDownloadIds.includes(id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'voice' ? (
|
) : resultType === 'voice' ? (
|
||||||
@ -353,6 +358,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
className="scroll-item"
|
className="scroll-item"
|
||||||
onPlay={handlePlayAudio}
|
onPlay={handlePlayAudio}
|
||||||
onDateClick={handleMessageFocus}
|
onDateClick={handleMessageFocus}
|
||||||
|
isDownloading={activeDownloadIds.includes(id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : resultType === 'members' ? (
|
) : resultType === 'members' ? (
|
||||||
@ -465,6 +471,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const canAddMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'inviteUsers') || chat.isCreator);
|
const canAddMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'inviteUsers') || chat.isCreator);
|
||||||
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
|
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
|
||||||
|
|
||||||
|
const activeDownloadIds = selectActiveDownloadIds(global, chatId);
|
||||||
|
|
||||||
let resolvedUserId;
|
let resolvedUserId;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
resolvedUserId = userId;
|
resolvedUserId = userId;
|
||||||
@ -488,6 +496,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isRestricted: chat?.isRestricted,
|
isRestricted: chat?.isRestricted,
|
||||||
lastSyncTime: global.lastSyncTime,
|
lastSyncTime: global.lastSyncTime,
|
||||||
serverTimeOffset: global.serverTimeOffset,
|
serverTimeOffset: global.serverTimeOffset,
|
||||||
|
activeDownloadIds,
|
||||||
usersById,
|
usersById,
|
||||||
chatsById,
|
chatsById,
|
||||||
...(hasMembersTab && members && { members }),
|
...(hasMembersTab && members && { members }),
|
||||||
|
|||||||
@ -121,6 +121,12 @@ function readCache(initialState: GlobalState): GlobalState {
|
|||||||
if (!cached.stickers.greeting) {
|
if (!cached.stickers.greeting) {
|
||||||
cached.stickers.greeting = initialState.stickers.greeting;
|
cached.stickers.greeting = initialState.stickers.greeting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.activeDownloads) {
|
||||||
|
cached.activeDownloads = {
|
||||||
|
byChatId: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
|
|||||||
@ -163,4 +163,8 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
twoFaSettings: {},
|
twoFaSettings: {},
|
||||||
|
|
||||||
shouldShowContextMenuHint: true,
|
shouldShowContextMenuHint: true,
|
||||||
|
|
||||||
|
activeDownloads: {
|
||||||
|
byChatId: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -438,6 +438,10 @@ export type GlobalState = {
|
|||||||
historyCalendarSelectedAt?: number;
|
historyCalendarSelectedAt?: number;
|
||||||
openedStickerSetShortName?: string;
|
openedStickerSetShortName?: string;
|
||||||
|
|
||||||
|
activeDownloads: {
|
||||||
|
byChatId: Record<number, number[]>;
|
||||||
|
};
|
||||||
|
|
||||||
shouldShowContextMenuHint?: boolean;
|
shouldShowContextMenuHint?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -470,6 +474,8 @@ export type ActionTypes = (
|
|||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||||
'reportMessages' | 'focusNextReply' | 'openChatByInvite' |
|
'reportMessages' | 'focusNextReply' | 'openChatByInvite' |
|
||||||
|
// downloads
|
||||||
|
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
||||||
// poll result
|
// poll result
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { getDispatch } from '../lib/teact/teactn';
|
|||||||
|
|
||||||
import { AudioOrigin } from '../types';
|
import { AudioOrigin } from '../types';
|
||||||
|
|
||||||
import { register, Track } from '../util/audioPlayer';
|
import { register, Track, TrackId } from '../util/audioPlayer';
|
||||||
import useEffectWithPrevDeps from './useEffectWithPrevDeps';
|
import useEffectWithPrevDeps from './useEffectWithPrevDeps';
|
||||||
import { isSafariPatchInProgress } from '../util/patchSafariProgressiveAudio';
|
import { isSafariPatchInProgress } from '../util/patchSafariProgressiveAudio';
|
||||||
import useOnChange from './useOnChange';
|
import useOnChange from './useOnChange';
|
||||||
@ -18,7 +18,7 @@ type Handler = (e: Event) => void;
|
|||||||
const DEFAULT_SKIP_TIME = 10;
|
const DEFAULT_SKIP_TIME = 10;
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
trackId: string,
|
trackId: TrackId,
|
||||||
originalDuration: number, // Sometimes incorrect for voice messages
|
originalDuration: number, // Sometimes incorrect for voice messages
|
||||||
trackType: Track['type'],
|
trackType: Track['type'],
|
||||||
origin: AudioOrigin,
|
origin: AudioOrigin,
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
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<HTMLElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDownloadStarted((isAllowed) => !isAllowed);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDownloadStarted,
|
|
||||||
downloadProgress,
|
|
||||||
handleDownloadClick,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -7,12 +7,13 @@ import { ApiMediaFormat } from '../api/types';
|
|||||||
import { throttle } from '../util/schedulers';
|
import { throttle } from '../util/schedulers';
|
||||||
import * as mediaLoader from '../util/mediaLoader';
|
import * as mediaLoader from '../util/mediaLoader';
|
||||||
import useForceUpdate from './useForceUpdate';
|
import useForceUpdate from './useForceUpdate';
|
||||||
|
import useUniqueId from './useUniqueId';
|
||||||
|
|
||||||
const STREAMING_PROGRESS = 0.75;
|
const STREAMING_PROGRESS = 0.75;
|
||||||
const STREAMING_TIMEOUT = 1500;
|
const STREAMING_TIMEOUT = 1500;
|
||||||
const PROGRESS_THROTTLE = 500;
|
const PROGRESS_THROTTLE = 500;
|
||||||
|
|
||||||
export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
export default function useMediaWithLoadProgress<T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
||||||
mediaHash: string | undefined,
|
mediaHash: string | undefined,
|
||||||
noLoad = false,
|
noLoad = false,
|
||||||
// @ts-ignore (workaround for "could be instantiated with a different subtype" issue)
|
// @ts-ignore (workaround for "could be instantiated with a different subtype" issue)
|
||||||
@ -20,19 +21,20 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
|||||||
cacheBuster?: number,
|
cacheBuster?: number,
|
||||||
delay?: number | false,
|
delay?: number | false,
|
||||||
isHtmlAllowed = false,
|
isHtmlAllowed = false,
|
||||||
) => {
|
) {
|
||||||
const mediaData = mediaHash ? mediaLoader.getFromMemory<T>(mediaHash) : undefined;
|
const mediaData = mediaHash ? mediaLoader.getFromMemory<T>(mediaHash) : undefined;
|
||||||
const isStreaming = mediaFormat === ApiMediaFormat.Stream || (
|
const isStreaming = mediaFormat === ApiMediaFormat.Stream || (
|
||||||
IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive
|
IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive
|
||||||
);
|
);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
const [downloadProgress, setDownloadProgress] = useState(mediaData && !isStreaming ? 1 : 0);
|
const id = useUniqueId();
|
||||||
|
const [loadProgress, setLoadProgress] = useState(mediaData && !isStreaming ? 1 : 0);
|
||||||
const startedAtRef = useRef<number>();
|
const startedAtRef = useRef<number>();
|
||||||
|
|
||||||
const handleProgress = useMemo(() => {
|
const handleProgress = useMemo(() => {
|
||||||
return throttle((progress: number) => {
|
return throttle((progress: number) => {
|
||||||
if (!delay || (Date.now() - startedAtRef.current! > delay)) {
|
if (startedAtRef.current && (!delay || (Date.now() - startedAtRef.current > delay))) {
|
||||||
setDownloadProgress(progress);
|
setLoadProgress(progress);
|
||||||
}
|
}
|
||||||
}, PROGRESS_THROTTLE, true);
|
}, PROGRESS_THROTTLE, true);
|
||||||
}, [delay]);
|
}, [delay]);
|
||||||
@ -40,7 +42,7 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noLoad && mediaHash) {
|
if (!noLoad && mediaHash) {
|
||||||
if (!mediaData) {
|
if (!mediaData) {
|
||||||
setDownloadProgress(0);
|
setLoadProgress(0);
|
||||||
|
|
||||||
if (startedAtRef.current) {
|
if (startedAtRef.current) {
|
||||||
mediaLoader.cancelProgress(handleProgress);
|
mediaLoader.cancelProgress(handleProgress);
|
||||||
@ -48,7 +50,7 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
|||||||
|
|
||||||
startedAtRef.current = Date.now();
|
startedAtRef.current = Date.now();
|
||||||
|
|
||||||
mediaLoader.fetch(mediaHash, mediaFormat, isHtmlAllowed, handleProgress).then(() => {
|
mediaLoader.fetch(mediaHash, mediaFormat, isHtmlAllowed, handleProgress, id).then(() => {
|
||||||
const spentTime = Date.now() - startedAtRef.current!;
|
const spentTime = Date.now() - startedAtRef.current!;
|
||||||
startedAtRef.current = undefined;
|
startedAtRef.current = undefined;
|
||||||
|
|
||||||
@ -60,21 +62,30 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
|||||||
});
|
});
|
||||||
} else if (isStreaming) {
|
} else if (isStreaming) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDownloadProgress(STREAMING_PROGRESS);
|
setLoadProgress(STREAMING_PROGRESS);
|
||||||
}, STREAMING_TIMEOUT);
|
}, STREAMING_TIMEOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, isStreaming, delay, handleProgress,
|
noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, isStreaming, delay, handleProgress,
|
||||||
isHtmlAllowed,
|
isHtmlAllowed, id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (noLoad && startedAtRef.current) {
|
if (noLoad && startedAtRef.current) {
|
||||||
mediaLoader.cancelProgress(handleProgress);
|
mediaLoader.cancelProgress(handleProgress);
|
||||||
setDownloadProgress(0);
|
setLoadProgress(0);
|
||||||
|
startedAtRef.current = undefined;
|
||||||
}
|
}
|
||||||
}, [handleProgress, noLoad]);
|
}, [handleProgress, noLoad]);
|
||||||
|
|
||||||
return { mediaData, downloadProgress };
|
useEffect(() => {
|
||||||
};
|
return () => {
|
||||||
|
if (mediaHash) {
|
||||||
|
mediaLoader.removeCallback(mediaHash, id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [id, mediaHash]);
|
||||||
|
|
||||||
|
return { mediaData, loadProgress };
|
||||||
|
}
|
||||||
15
src/hooks/useUniqueId.ts
Normal file
15
src/hooks/useUniqueId.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useRef } from '../lib/teact/teact';
|
||||||
|
import generateIdFor from '../util/generateIdFor';
|
||||||
|
|
||||||
|
const store: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const idRef = useRef<string>();
|
||||||
|
|
||||||
|
if (!idRef.current) {
|
||||||
|
idRef.current = generateIdFor(store);
|
||||||
|
store[idRef.current] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return idRef.current;
|
||||||
|
};
|
||||||
@ -394,6 +394,53 @@ addReducer('openForwardMenuForSelectedMessages', (global, actions) => {
|
|||||||
actions.openForwardMenu({ fromChatId, messageIds });
|
actions.openForwardMenu({ fromChatId, messageIds });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('cancelMessageMediaDownload', (global, actions, payload) => {
|
||||||
|
const { message } = payload!;
|
||||||
|
|
||||||
|
const byChatId = global.activeDownloads.byChatId[message.chatId];
|
||||||
|
if (!byChatId || !byChatId.length) return;
|
||||||
|
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
activeDownloads: {
|
||||||
|
byChatId: {
|
||||||
|
...global.activeDownloads.byChatId,
|
||||||
|
[message.chatId]: byChatId.filter((id) => id !== message.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('downloadMessageMedia', (global, actions, payload) => {
|
||||||
|
const { message } = payload!;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
activeDownloads: {
|
||||||
|
byChatId: {
|
||||||
|
...global.activeDownloads.byChatId,
|
||||||
|
[message.chatId]: [...(global.activeDownloads.byChatId[message.chatId] || []), message.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('downloadSelectedMessages', (global, actions) => {
|
||||||
|
if (!global.selectedMessages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chatId, messageIds } = global.selectedMessages;
|
||||||
|
const { threadId } = selectCurrentMessageList(global) || {};
|
||||||
|
|
||||||
|
const chatMessages = selectChatMessages(global, chatId);
|
||||||
|
if (!chatMessages || !threadId) return;
|
||||||
|
const messages = messageIds.map((id) => chatMessages[id])
|
||||||
|
.filter((message) => selectAllowedMessageActions(global, message, threadId).canDownload);
|
||||||
|
messages.forEach((message) => actions.downloadMessageMedia({ message }));
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('enterMessageSelectMode', (global, actions, payload) => {
|
addReducer('enterMessageSelectMode', (global, actions, payload) => {
|
||||||
const { messageId } = payload || {};
|
const { messageId } = payload || {};
|
||||||
const openChat = selectCurrentChat(global);
|
const openChat = selectCurrentChat(global);
|
||||||
|
|||||||
@ -168,6 +168,7 @@ export function getMessageMediaHash(
|
|||||||
case 'viewerPreview':
|
case 'viewerPreview':
|
||||||
return `${base}?size=x`;
|
return `${base}?size=x`;
|
||||||
case 'viewerFull':
|
case 'viewerFull':
|
||||||
|
case 'download':
|
||||||
return `${base}?size=z`;
|
return `${base}?size=z`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +184,7 @@ export function getMessageMediaHash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `${base}?size=m`;
|
return `${base}?size=m`;
|
||||||
default:
|
case 'download':
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,8 +195,10 @@ export function getMessageMediaHash(
|
|||||||
return undefined;
|
return undefined;
|
||||||
case 'pictogram':
|
case 'pictogram':
|
||||||
return `${base}?size=m`;
|
return `${base}?size=m`;
|
||||||
default:
|
case 'inline':
|
||||||
return base;
|
return base;
|
||||||
|
case 'download':
|
||||||
|
return `${base}?download`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,10 +207,10 @@ export function getMessageMediaHash(
|
|||||||
case 'micro':
|
case 'micro':
|
||||||
case 'pictogram':
|
case 'pictogram':
|
||||||
return getAudioHasCover(audio) ? `${base}?size=m` : undefined;
|
return getAudioHasCover(audio) ? `${base}?size=m` : undefined;
|
||||||
|
case 'inline':
|
||||||
|
return getVideoOrAudioBaseHash(audio, base);
|
||||||
case 'download':
|
case 'download':
|
||||||
return `${base}?download`;
|
return `${base}?download`;
|
||||||
default:
|
|
||||||
return getVideoOrAudioBaseHash(audio, base);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,8 +219,10 @@ export function getMessageMediaHash(
|
|||||||
case 'micro':
|
case 'micro':
|
||||||
case 'pictogram':
|
case 'pictogram':
|
||||||
return undefined;
|
return undefined;
|
||||||
default:
|
case 'inline':
|
||||||
return base;
|
return base;
|
||||||
|
case 'download':
|
||||||
|
return `${base}?download`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,9 +335,9 @@ export function getVideoDimensions(video: ApiVideo): ApiDimensions | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMediaTransferState(message: ApiMessage, progress?: number, isDownloadNeeded = false) {
|
export function getMediaTransferState(message: ApiMessage, progress?: number, isLoadNeeded = false) {
|
||||||
const isUploading = isMessageLocal(message);
|
const isUploading = isMessageLocal(message);
|
||||||
const isTransferring = isUploading || isDownloadNeeded;
|
const isTransferring = isUploading || isLoadNeeded;
|
||||||
const transferProgress = Number(progress);
|
const transferProgress = Number(progress);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -4,20 +4,25 @@ import {
|
|||||||
import { LangFn } from '../../hooks/useLang';
|
import { LangFn } from '../../hooks/useLang';
|
||||||
|
|
||||||
import { LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIONS_USER_ID, RE_LINK_TEMPLATE } from '../../config';
|
import { LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIONS_USER_ID, RE_LINK_TEMPLATE } from '../../config';
|
||||||
import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString';
|
|
||||||
import { getUserFullName } from './users';
|
import { getUserFullName } from './users';
|
||||||
|
import { isWebpSupported, IS_OPUS_SUPPORTED } from '../../util/environment';
|
||||||
import { getChatTitle } from './chats';
|
import { getChatTitle } from './chats';
|
||||||
|
import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString';
|
||||||
|
|
||||||
const CONTENT_NOT_SUPPORTED = 'The message is not supported on this version of Telegram';
|
const CONTENT_NOT_SUPPORTED = 'The message is not supported on this version of Telegram';
|
||||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||||
const TRUNCATED_SUMMARY_LENGTH = 80;
|
const TRUNCATED_SUMMARY_LENGTH = 80;
|
||||||
|
|
||||||
export type MessageKey = string; // `msg${number}-${number}`;
|
export type MessageKey = `msg${number}-${number}`;
|
||||||
|
|
||||||
export function getMessageKey(message: ApiMessage): MessageKey {
|
export function getMessageKey(message: ApiMessage): MessageKey {
|
||||||
const { chatId, id } = message;
|
const { chatId, id } = message;
|
||||||
|
|
||||||
return `msg${chatId}-${id}`;
|
return buildMessageKey(chatId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessageKey(chatId: number, msgId: number): MessageKey {
|
||||||
|
return `msg${chatId}-${msgId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseMessageKey(key: MessageKey) {
|
export function parseMessageKey(key: MessageKey) {
|
||||||
@ -226,3 +231,39 @@ export function getMessageAudioCaption(message: ApiMessage) {
|
|||||||
|
|
||||||
return (audio && [audio.title, audio.performer].filter(Boolean).join(' — ')) || (text?.text);
|
return (audio && [audio.title, audio.performer].filter(Boolean).join(' — ')) || (text?.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMessageContentFilename(message: ApiMessage) {
|
||||||
|
const { content } = message;
|
||||||
|
|
||||||
|
const video = content.webPage ? content.webPage.video : content.video;
|
||||||
|
const photo = content.webPage ? content.webPage.photo : content.photo;
|
||||||
|
const document = content.webPage ? content.webPage.document : content.document;
|
||||||
|
if (document) {
|
||||||
|
return document.fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video) {
|
||||||
|
return video.fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.sticker) {
|
||||||
|
const extension = content.sticker.isAnimated ? 'tgs' : isWebpSupported() ? 'webp' : 'png';
|
||||||
|
return `${content.sticker.id}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.audio) {
|
||||||
|
return content.audio.fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFilename = getMessageKey(message);
|
||||||
|
|
||||||
|
if (photo) {
|
||||||
|
return `${baseFilename}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.voice) {
|
||||||
|
return IS_OPUS_SUPPORTED ? `${baseFilename}.ogg` : `${baseFilename}.wav`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFilename;
|
||||||
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
import { findLast } from '../../util/iteratees';
|
import { findLast } from '../../util/iteratees';
|
||||||
import { selectIsStickerFavorite } from './symbols';
|
import { selectIsStickerFavorite } from './symbols';
|
||||||
import { getServerTime } from '../../util/serverTime';
|
import { getServerTime } from '../../util/serverTime';
|
||||||
|
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||||
|
|
||||||
const MESSAGE_EDIT_ALLOWED_TIME = 172800; // 48 hours
|
const MESSAGE_EDIT_ALLOWED_TIME = 172800; // 48 hours
|
||||||
|
|
||||||
@ -413,11 +414,16 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
const canCopy = !isAction;
|
const canCopy = !isAction;
|
||||||
const canCopyLink = !isAction && (isChannel || isSuperGroup);
|
const canCopyLink = !isAction && (isChannel || isSuperGroup);
|
||||||
const canSelect = !isAction;
|
const canSelect = !isAction;
|
||||||
|
|
||||||
|
const canDownload = Boolean(content.webPage?.document || content.webPage?.video || content.webPage?.photo
|
||||||
|
|| content.audio || content.voice || content.photo || content.video || content.document || content.sticker);
|
||||||
|
|
||||||
const noOptions = [
|
const noOptions = [
|
||||||
canReply,
|
canReply,
|
||||||
canEdit,
|
canEdit,
|
||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
|
canReport,
|
||||||
canDelete,
|
canDelete,
|
||||||
canDeleteForAll,
|
canDeleteForAll,
|
||||||
canForward,
|
canForward,
|
||||||
@ -426,6 +432,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canCopy,
|
canCopy,
|
||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
|
canDownload,
|
||||||
].every((ability) => !ability);
|
].every((ability) => !ability);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -434,8 +441,8 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canEdit,
|
canEdit,
|
||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
canDelete,
|
|
||||||
canReport,
|
canReport,
|
||||||
|
canDelete,
|
||||||
canDeleteForAll,
|
canDeleteForAll,
|
||||||
canForward,
|
canForward,
|
||||||
canFaveSticker,
|
canFaveSticker,
|
||||||
@ -443,6 +450,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canCopy,
|
canCopy,
|
||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
|
canDownload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,6 +488,30 @@ export function selectCanReportSelectedMessages(global: GlobalState) {
|
|||||||
return messageActions.every((actions) => actions.canReport);
|
return messageActions.every((actions) => actions.canReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectCanDownloadSelectedMessages(global: GlobalState) {
|
||||||
|
const { messageIds: selectedMessageIds } = global.selectedMessages || {};
|
||||||
|
const { chatId, threadId } = selectCurrentMessageList(global) || {};
|
||||||
|
const chatMessages = chatId && selectChatMessages(global, chatId);
|
||||||
|
if (!chatMessages || !selectedMessageIds || !threadId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageActions = selectedMessageIds
|
||||||
|
.map((id) => chatMessages[id] && selectAllowedMessageActions(global, chatMessages[id], threadId))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return messageActions.some((actions) => actions.canDownload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectIsDownloading(global: GlobalState, message: ApiMessage) {
|
||||||
|
const activeInChat = global.activeDownloads.byChatId[message.chatId];
|
||||||
|
return activeInChat ? activeInChat.includes(message.id) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectActiveDownloadIds(global: GlobalState, chatId: number) {
|
||||||
|
return global.activeDownloads.byChatId[chatId] || MEMO_EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
export function selectUploadProgress(global: GlobalState, message: ApiMessage) {
|
export function selectUploadProgress(global: GlobalState, message: ApiMessage) {
|
||||||
return global.fileUploads.byMessageLocalId[message.previousLocalId || message.id]?.progress;
|
return global.fileUploads.byMessageLocalId[message.previousLocalId || message.id]?.progress;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { ApiMessage } from '../api/types';
|
|||||||
import { IS_SAFARI } from './environment';
|
import { IS_SAFARI } from './environment';
|
||||||
import safePlay from './safePlay';
|
import safePlay from './safePlay';
|
||||||
import { patchSafariProgressiveAudio, isSafariPatchInProgress } from './patchSafariProgressiveAudio';
|
import { patchSafariProgressiveAudio, isSafariPatchInProgress } from './patchSafariProgressiveAudio';
|
||||||
import { getMessageKey, parseMessageKey } from '../modules/helpers';
|
import { getMessageKey, MessageKey, parseMessageKey } from '../modules/helpers';
|
||||||
import { fastRaf } from './schedulers';
|
import { fastRaf } from './schedulers';
|
||||||
|
|
||||||
type Handler = (eventName: string, e: Event) => void;
|
type Handler = (eventName: string, e: Event) => void;
|
||||||
type TrackId = string; // `${MessageKey}-${number}`;
|
export type TrackId = `${MessageKey}-${number}`;
|
||||||
|
|
||||||
export interface Track {
|
export interface Track {
|
||||||
audio: HTMLAudioElement;
|
audio: HTMLAudioElement;
|
||||||
@ -131,7 +131,7 @@ export function stopCurrentAudio() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function register(
|
export function register(
|
||||||
trackId: string,
|
trackId: TrackId,
|
||||||
trackType: Track['type'],
|
trackType: Track['type'],
|
||||||
origin: AudioOrigin,
|
origin: AudioOrigin,
|
||||||
handler: Handler,
|
handler: Handler,
|
||||||
@ -317,7 +317,7 @@ export function makeTrackId(message: ApiMessage): TrackId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function splitTrackId(trackId: TrackId) {
|
function splitTrackId(trackId: TrackId) {
|
||||||
const messageKey = trackId.match(/^msg(-?\d+)-(\d+)/)![0];
|
const messageKey = trackId.match(/^msg(-?\d+)-(\d+)/)![0] as MessageKey;
|
||||||
const date = Number(trackId.split('-').pop());
|
const date = Number(trackId.split('-').pop());
|
||||||
return {
|
return {
|
||||||
messageKey,
|
messageKey,
|
||||||
|
|||||||
@ -2,5 +2,10 @@ export default function download(url: string, filename: string) {
|
|||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
link.click();
|
try {
|
||||||
|
link.click();
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err); // Suppress redundant "Blob loading failed" error popup on IOS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,20 +27,26 @@ const PROGRESSIVE_URL_PREFIX = './progressive/';
|
|||||||
|
|
||||||
const memoryCache = new Map<string, ApiPreparedMedia>();
|
const memoryCache = new Map<string, ApiPreparedMedia>();
|
||||||
const fetchPromises = new Map<string, Promise<ApiPreparedMedia | undefined>>();
|
const fetchPromises = new Map<string, Promise<ApiPreparedMedia | undefined>>();
|
||||||
|
const progressCallbacks = new Map<string, Map<string, ApiOnProgress>>();
|
||||||
|
const cancellableCallbacks = new Map<string, ApiOnProgress>();
|
||||||
|
|
||||||
export function fetch<T extends ApiMediaFormat>(
|
export function fetch<T extends ApiMediaFormat>(
|
||||||
url: string, mediaFormat: T, isHtmlAllowed = false, onProgress?: ApiOnProgress,
|
url: string,
|
||||||
|
mediaFormat: T,
|
||||||
|
isHtmlAllowed = false,
|
||||||
|
onProgress?: ApiOnProgress,
|
||||||
|
callbackUniqueId?: string,
|
||||||
): Promise<ApiMediaFormatToPrepared<T>> {
|
): Promise<ApiMediaFormatToPrepared<T>> {
|
||||||
if (mediaFormat === ApiMediaFormat.Progressive) {
|
if (mediaFormat === ApiMediaFormat.Progressive) {
|
||||||
return (
|
return (
|
||||||
IS_PROGRESSIVE_SUPPORTED
|
IS_PROGRESSIVE_SUPPORTED
|
||||||
? getProgressive(url)
|
? getProgressive(url)
|
||||||
: fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress)
|
: fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress, callbackUniqueId)
|
||||||
) as Promise<ApiMediaFormatToPrepared<T>>;
|
) as Promise<ApiMediaFormatToPrepared<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchPromises.has(url)) {
|
if (!fetchPromises.has(url)) {
|
||||||
const promise = fetchFromCacheOrRemote(url, mediaFormat, isHtmlAllowed, onProgress)
|
const promise = fetchFromCacheOrRemote(url, mediaFormat, isHtmlAllowed)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -51,11 +57,22 @@ export function fetch<T extends ApiMediaFormat>(
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
fetchPromises.delete(url);
|
fetchPromises.delete(url);
|
||||||
|
progressCallbacks.delete(url);
|
||||||
|
cancellableCallbacks.delete(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchPromises.set(url, promise);
|
fetchPromises.set(url, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onProgress && callbackUniqueId) {
|
||||||
|
let activeCallbacks = progressCallbacks.get(url);
|
||||||
|
if (!activeCallbacks) {
|
||||||
|
activeCallbacks = new Map<string, ApiOnProgress>();
|
||||||
|
progressCallbacks.set(url, activeCallbacks);
|
||||||
|
}
|
||||||
|
activeCallbacks.set(callbackUniqueId, onProgress);
|
||||||
|
}
|
||||||
|
|
||||||
return fetchPromises.get(url) as Promise<ApiMediaFormatToPrepared<T>>;
|
return fetchPromises.get(url) as Promise<ApiMediaFormatToPrepared<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +81,23 @@ export function getFromMemory<T extends ApiMediaFormat>(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cancelProgress(progressCallback: ApiOnProgress) {
|
export function cancelProgress(progressCallback: ApiOnProgress) {
|
||||||
cancelApiProgress(progressCallback);
|
progressCallbacks.forEach((map, url) => {
|
||||||
|
map.forEach((callback) => {
|
||||||
|
if (callback === progressCallback) {
|
||||||
|
const parentCallback = cancellableCallbacks.get(url)!;
|
||||||
|
|
||||||
|
cancelApiProgress(parentCallback);
|
||||||
|
cancellableCallbacks.delete(url);
|
||||||
|
progressCallbacks.delete(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCallback(url: string, callbackUniqueId: string) {
|
||||||
|
const callbacks = progressCallbacks.get(url);
|
||||||
|
if (!callbacks) return;
|
||||||
|
callbacks.delete(callbackUniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgressive(url: string) {
|
function getProgressive(url: string) {
|
||||||
@ -76,7 +109,7 @@ function getProgressive(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFromCacheOrRemote(
|
async function fetchFromCacheOrRemote(
|
||||||
url: string, mediaFormat: ApiMediaFormat, isHtmlAllowed: boolean, onProgress?: ApiOnProgress,
|
url: string, mediaFormat: ApiMediaFormat, isHtmlAllowed: boolean,
|
||||||
) {
|
) {
|
||||||
if (!MEDIA_CACHE_DISABLED) {
|
if (!MEDIA_CACHE_DISABLED) {
|
||||||
const cacheName = url.startsWith('avatar') ? MEDIA_CACHE_NAME_AVATARS : MEDIA_CACHE_NAME;
|
const cacheName = url.startsWith('avatar') ? MEDIA_CACHE_NAME_AVATARS : MEDIA_CACHE_NAME;
|
||||||
@ -117,30 +150,22 @@ async function fetchFromCacheOrRemote(
|
|||||||
|
|
||||||
const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
|
const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
|
||||||
|
|
||||||
void callApi('downloadMedia', { url, mediaFormat }, (progress: number, arrayBuffer: ArrayBuffer) => {
|
const onProgress = makeOnProgress(url, mediaSource, sourceBuffer);
|
||||||
if (onProgress) {
|
cancellableCallbacks.set(url, onProgress);
|
||||||
onProgress(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress === 1) {
|
void callApi('downloadMedia', { url, mediaFormat }, onProgress);
|
||||||
mediaSource.endOfStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!arrayBuffer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceBuffer.appendBuffer(arrayBuffer!);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
memoryCache.set(url, streamUrl);
|
memoryCache.set(url, streamUrl);
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onProgress = makeOnProgress(url);
|
||||||
|
cancellableCallbacks.set(url, onProgress);
|
||||||
|
|
||||||
const remote = await callApi('downloadMedia', { url, mediaFormat, isHtmlAllowed }, onProgress);
|
const remote = await callApi('downloadMedia', { url, mediaFormat, isHtmlAllowed }, onProgress);
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
throw new Error('Failed to fetch media');
|
throw new Error(`Failed to fetch media ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let { prepared, mimeType } = remote;
|
let { prepared, mimeType } = remote;
|
||||||
@ -150,7 +175,7 @@ async function fetchFromCacheOrRemote(
|
|||||||
URL.revokeObjectURL(prepared as string);
|
URL.revokeObjectURL(prepared as string);
|
||||||
const media = await oggToWav(blob);
|
const media = await oggToWav(blob);
|
||||||
prepared = prepareMedia(media);
|
prepared = prepareMedia(media);
|
||||||
mimeType = blob.type;
|
mimeType = media.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mimeType === 'image/webp' && !isWebpSupported()) {
|
if (mimeType === 'image/webp' && !isWebpSupported()) {
|
||||||
@ -167,6 +192,27 @@ async function fetchFromCacheOrRemote(
|
|||||||
return prepared;
|
return prepared;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeOnProgress(url: string, mediaSource?: MediaSource, sourceBuffer?: SourceBuffer) {
|
||||||
|
const onProgress: ApiOnProgress = (progress: number, arrayBuffer: ArrayBuffer) => {
|
||||||
|
progressCallbacks.get(url)?.forEach((callback) => {
|
||||||
|
callback(progress);
|
||||||
|
if (callback.isCanceled) onProgress.isCanceled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (progress === 1) {
|
||||||
|
mediaSource?.endOfStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceBuffer?.appendBuffer(arrayBuffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return onProgress;
|
||||||
|
}
|
||||||
|
|
||||||
function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
||||||
if (mediaData instanceof Blob) {
|
if (mediaData instanceof Blob) {
|
||||||
return URL.createObjectURL(mediaData);
|
return URL.createObjectURL(mediaData);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user