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