Downloads: Fallback to Service Worker when OPFS not supported (#1949)
This commit is contained in:
parent
e57bf3518d
commit
2a5eeb3111
@ -50,7 +50,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
|||||||
onUpdate = _onUpdate;
|
onUpdate = _onUpdate;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported,
|
userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported, maxBufferSize,
|
||||||
} = initialArgs;
|
} = initialArgs;
|
||||||
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
|
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
|
||||||
|
|
||||||
@ -60,6 +60,8 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
|||||||
if (isMovSupported) SUPPORTED_VIDEO_CONTENT_TYPES.add(VIDEO_MOV_TYPE);
|
if (isMovSupported) SUPPORTED_VIDEO_CONTENT_TYPES.add(VIDEO_MOV_TYPE);
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
(self as any).isWebmSupported = isWebmSupported;
|
(self as any).isWebmSupported = isWebmSupported;
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
(self as any).maxBufferSize = maxBufferSize;
|
||||||
|
|
||||||
client = new TelegramClient(
|
client = new TelegramClient(
|
||||||
session,
|
session,
|
||||||
|
|||||||
@ -253,6 +253,7 @@ async function parseMedia(
|
|||||||
case ApiMediaFormat.Text:
|
case ApiMediaFormat.Text:
|
||||||
return data.toString();
|
return data.toString();
|
||||||
case ApiMediaFormat.Progressive:
|
case ApiMediaFormat.Progressive:
|
||||||
|
case ApiMediaFormat.DownloadUrl:
|
||||||
return data.buffer;
|
return data.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export enum ApiMediaFormat {
|
|||||||
BlobUrl,
|
BlobUrl,
|
||||||
Progressive,
|
Progressive,
|
||||||
Stream,
|
Stream,
|
||||||
|
DownloadUrl,
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface ApiInitialArgs {
|
|||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
isMovSupported?: boolean;
|
isMovSupported?: boolean;
|
||||||
isWebmSupported?: boolean;
|
isWebmSupported?: boolean;
|
||||||
|
maxBufferSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiOnProgress {
|
export interface ApiOnProgress {
|
||||||
|
|||||||
@ -125,6 +125,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
getMessageMediaHash(message, 'download'),
|
getMessageMediaHash(message, 'download'),
|
||||||
!isDownloading,
|
!isDownloading,
|
||||||
|
getMessageMediaFormat(message, 'download'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleForcePlay = useCallback(() => {
|
const handleForcePlay = useCallback(() => {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import type { ApiMessage } from '../../api/types';
|
|||||||
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
|
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
|
||||||
import {
|
import {
|
||||||
getMediaTransferState,
|
getMediaTransferState,
|
||||||
|
getMessageMediaFormat,
|
||||||
getMessageMediaHash,
|
getMessageMediaHash,
|
||||||
getMessageMediaThumbDataUri,
|
getMessageMediaThumbDataUri,
|
||||||
isMessageDocumentVideo,
|
isMessageDocumentVideo,
|
||||||
@ -86,7 +87,7 @@ const Document: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const documentHash = getMessageMediaHash(message, 'download');
|
const documentHash = getMessageMediaHash(message, 'download');
|
||||||
const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress(
|
const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress(
|
||||||
documentHash, !shouldDownload, undefined, undefined, undefined, true,
|
documentHash, !shouldDownload, getMessageMediaFormat(message, 'download'), undefined, undefined, true,
|
||||||
);
|
);
|
||||||
const isLoaded = Boolean(mediaData);
|
const isLoaded = Boolean(mediaData);
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import type { Thread } from '../../global/types';
|
|||||||
import type { ApiMessage } from '../../api/types';
|
import type { ApiMessage } from '../../api/types';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
|
|
||||||
import { IS_OPFS_SUPPORTED } from '../../util/environment';
|
import { IS_OPFS_SUPPORTED, IS_SERVICE_WORKER_SUPPORTED, MAX_BUFFER_SIZE } from '../../util/environment';
|
||||||
import * as mediaLoader from '../../util/mediaLoader';
|
import * as mediaLoader from '../../util/mediaLoader';
|
||||||
import download from '../../util/download';
|
import download from '../../util/download';
|
||||||
import {
|
import {
|
||||||
getMessageContentFilename, getMessageMediaHash,
|
getMessageContentFilename, getMessageMediaFormat, getMessageMediaHash,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
|
|
||||||
import useRunDebounced from '../../hooks/useRunDebounced';
|
import useRunDebounced from '../../hooks/useRunDebounced';
|
||||||
@ -25,8 +25,6 @@ type StateProps = {
|
|||||||
|
|
||||||
const GLOBAL_UPDATE_DEBOUNCE = 1000;
|
const GLOBAL_UPDATE_DEBOUNCE = 1000;
|
||||||
|
|
||||||
const MAX_BLOB_SAFE_SIZE = 2000 * 1024 * 1024;
|
|
||||||
|
|
||||||
const processedMessages = new Set<ApiMessage>();
|
const processedMessages = new Set<ApiMessage>();
|
||||||
const downloadedMessages = new Set<ApiMessage>();
|
const downloadedMessages = new Set<ApiMessage>();
|
||||||
|
|
||||||
@ -81,18 +79,30 @@ const DownloadManager: FC<StateProps> = ({
|
|||||||
document, video, audio,
|
document, video, audio,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
const mediaSize = (document || video || audio)?.size || 0;
|
const mediaSize = (document || video || audio)?.size || 0;
|
||||||
if (mediaSize > MAX_BLOB_SAFE_SIZE && !IS_OPFS_SUPPORTED) {
|
if (mediaSize > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED && !IS_SERVICE_WORKER_SUPPORTED) {
|
||||||
showNotification({
|
showNotification({
|
||||||
message: 'Downloading files bigger than 2GB is currently not supported in your browser.',
|
message: 'Downloading files bigger than 2GB is not supported in your browser.',
|
||||||
});
|
});
|
||||||
handleMessageDownloaded(message);
|
handleMessageDownloaded(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaLoader.fetch(downloadHash, ApiMediaFormat.BlobUrl, true).then((result) => {
|
const mediaFormat = getMessageMediaFormat(message, 'download');
|
||||||
if (result) {
|
mediaLoader.fetch(downloadHash, mediaFormat, true).then((result) => {
|
||||||
|
if (mediaFormat === ApiMediaFormat.DownloadUrl) {
|
||||||
|
const url = new URL(result, window.document.baseURI);
|
||||||
|
const filename = getMessageContentFilename(message);
|
||||||
|
url.searchParams.set('filename', encodeURIComponent(filename));
|
||||||
|
const downloadWindow = window.open(url.toString());
|
||||||
|
downloadWindow?.addEventListener('beforeunload', () => {
|
||||||
|
showNotification({
|
||||||
|
message: 'Download started. Please, do not close the app before it is finished.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (result) {
|
||||||
download(result, getMessageContentFilename(message));
|
download(result, getMessageContentFilename(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMessageDownloaded(message);
|
handleMessageDownloaded(message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { formatFileSize } from '../../../../util/textFormat';
|
|||||||
import { getActions, withGlobal } from '../../../../global';
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
import { selectIsCurrentUserPremium, selectIsPremiumPurchaseBlocked } from '../../../../global/selectors';
|
import { selectIsCurrentUserPremium, selectIsPremiumPurchaseBlocked } from '../../../../global/selectors';
|
||||||
import useLang from '../../../../hooks/useLang';
|
import useLang from '../../../../hooks/useLang';
|
||||||
import { FILEPART_SIZE } from '../../../../config';
|
import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config';
|
||||||
import useFlag from '../../../../hooks/useFlag';
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
|
||||||
import Modal from '../../../ui/Modal';
|
import Modal from '../../../ui/Modal';
|
||||||
@ -60,7 +60,7 @@ const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any
|
|||||||
// The real size is not exactly 4gb, so we need to round it
|
// The real size is not exactly 4gb, so we need to round it
|
||||||
if (value === 8000) return lang('FileSize.GB', '4');
|
if (value === 8000) return lang('FileSize.GB', '4');
|
||||||
if (value === 4000) return lang('FileSize.GB', '2');
|
if (value === 4000) return lang('FileSize.GB', '2');
|
||||||
return formatFileSize(lang, value * FILEPART_SIZE);
|
return formatFileSize(lang, value * MAX_UPLOAD_FILEPART_SIZE);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { getActions, withGlobal } from '../../global';
|
|||||||
import type { ApiMessage } from '../../api/types';
|
import type { ApiMessage } from '../../api/types';
|
||||||
|
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import { getMessageMediaHash } from '../../global/helpers';
|
import { getMessageMediaFormat, getMessageMediaHash } from '../../global/helpers';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||||
import { selectIsDownloading, selectIsMessageProtected } from '../../global/selectors';
|
import { selectIsDownloading, selectIsMessageProtected } from '../../global/selectors';
|
||||||
@ -63,6 +63,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
message && getMessageMediaHash(message, 'download'),
|
message && getMessageMediaHash(message, 'download'),
|
||||||
!isDownloading,
|
!isDownloading,
|
||||||
|
message && getMessageMediaFormat(message, 'download'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDownloadClick = useCallback(() => {
|
const handleDownloadClick = useCallback(() => {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import {
|
|||||||
EDITABLE_INPUT_ID,
|
EDITABLE_INPUT_ID,
|
||||||
REPLIES_USER_ID,
|
REPLIES_USER_ID,
|
||||||
SEND_MESSAGE_ACTION_INTERVAL,
|
SEND_MESSAGE_ACTION_INTERVAL,
|
||||||
EDITABLE_INPUT_CSS_SELECTOR, FILEPART_SIZE,
|
EDITABLE_INPUT_CSS_SELECTOR, MAX_UPLOAD_FILEPART_SIZE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
@ -1355,7 +1355,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
attachMenuBots: global.attachMenu.bots,
|
attachMenuBots: global.attachMenu.bots,
|
||||||
attachMenuPeerType: selectAttachMenuPeerType(global, chatId),
|
attachMenuPeerType: selectAttachMenuPeerType(global, chatId),
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
fileSizeLimit: selectCurrentLimit(global, 'uploadMaxFileparts') * FILEPART_SIZE,
|
fileSizeLimit: selectCurrentLimit(global, 'uploadMaxFileparts') * MAX_UPLOAD_FILEPART_SIZE,
|
||||||
captionLimit: selectCurrentLimit(global, 'captionLength'),
|
captionLimit: selectCurrentLimit(global, 'captionLength'),
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
getMessageMediaHash,
|
getMessageMediaHash,
|
||||||
getMediaTransferState,
|
getMediaTransferState,
|
||||||
isOwnMessage,
|
isOwnMessage,
|
||||||
|
getMessageMediaFormat,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
@ -87,7 +88,9 @@ const Photo: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
loadProgress: downloadProgress,
|
loadProgress: downloadProgress,
|
||||||
} = useMediaWithLoadProgress(getMessageMediaHash(message, 'download'), !isDownloading);
|
} = useMediaWithLoadProgress(
|
||||||
|
getMessageMediaHash(message, 'download'), !isDownloading, getMessageMediaFormat(message, 'download'),
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isUploading, isTransferring, transferProgress,
|
isUploading, isTransferring, transferProgress,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import React, { useCallback, useRef, useState } from '../../../lib/teact/teact';
|
|||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { ApiMessage } from '../../../api/types';
|
import type { ApiMessage } from '../../../api/types';
|
||||||
import { ApiMediaFormat } from '../../../api/types';
|
|
||||||
import type { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
import type { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
||||||
|
|
||||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||||
@ -101,7 +100,7 @@ const Video: FC<OwnProps> = ({
|
|||||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||||
getMessageMediaHash(message, 'download'),
|
getMessageMediaHash(message, 'download'),
|
||||||
!isDownloading,
|
!isDownloading,
|
||||||
ApiMediaFormat.BlobUrl,
|
getMessageMediaFormat(message, 'download'),
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -214,7 +214,7 @@ export const LIGHT_THEME_BG_COLOR = '#99BA92';
|
|||||||
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
||||||
export const DEFAULT_PATTERN_COLOR = '#4A8E3A8C';
|
export const DEFAULT_PATTERN_COLOR = '#4A8E3A8C';
|
||||||
export const DARK_THEME_PATTERN_COLOR = '#0A0A0A8C';
|
export const DARK_THEME_PATTERN_COLOR = '#0A0A0A8C';
|
||||||
export const FILEPART_SIZE = 524288;
|
export const MAX_UPLOAD_FILEPART_SIZE = 524288;
|
||||||
|
|
||||||
// Group calls
|
// Group calls
|
||||||
export const GROUP_CALL_VOLUME_MULTIPLIER = 100;
|
export const GROUP_CALL_VOLUME_MULTIPLIER = 100;
|
||||||
|
|||||||
@ -13,7 +13,9 @@ import {
|
|||||||
IS_TEST,
|
IS_TEST,
|
||||||
LOCK_SCREEN_ANIMATION_DURATION_MS,
|
LOCK_SCREEN_ANIMATION_DURATION_MS,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, PLATFORM_ENV } from '../../../util/environment';
|
import {
|
||||||
|
IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, MAX_BUFFER_SIZE, PLATFORM_ENV,
|
||||||
|
} from '../../../util/environment';
|
||||||
import { unsubscribe } from '../../../util/notifications';
|
import { unsubscribe } from '../../../util/notifications';
|
||||||
import * as cacheApi from '../../../util/cacheApi';
|
import * as cacheApi from '../../../util/cacheApi';
|
||||||
import { updateAppBadge } from '../../../util/appBadge';
|
import { updateAppBadge } from '../../../util/appBadge';
|
||||||
@ -42,6 +44,7 @@ addActionHandler('initApi', async (global, actions) => {
|
|||||||
isTest: window.location.search.includes('test'),
|
isTest: window.location.search.includes('test'),
|
||||||
isMovSupported: IS_MOV_SUPPORTED,
|
isMovSupported: IS_MOV_SUPPORTED,
|
||||||
isWebmSupported: IS_WEBM_SUPPORTED,
|
isWebmSupported: IS_WEBM_SUPPORTED,
|
||||||
|
maxBufferSize: MAX_BUFFER_SIZE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,13 @@ import type {
|
|||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
|
|
||||||
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment';
|
import {
|
||||||
|
IS_OPFS_SUPPORTED,
|
||||||
|
IS_OPUS_SUPPORTED,
|
||||||
|
IS_PROGRESSIVE_SUPPORTED,
|
||||||
|
IS_SAFARI,
|
||||||
|
MAX_BUFFER_SIZE,
|
||||||
|
} from '../../util/environment';
|
||||||
import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages';
|
import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages';
|
||||||
import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo';
|
import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo';
|
||||||
|
|
||||||
@ -306,14 +312,22 @@ export function getAudioHasCover(media: ApiAudio) {
|
|||||||
export function getMessageMediaFormat(
|
export function getMessageMediaFormat(
|
||||||
message: ApiMessage, target: Target,
|
message: ApiMessage, target: Target,
|
||||||
): ApiMediaFormat {
|
): ApiMediaFormat {
|
||||||
const { video, audio, voice } = message.content;
|
const {
|
||||||
|
video, audio, voice, document,
|
||||||
|
} = message.content;
|
||||||
const fullVideo = video || getMessageWebPageVideo(message);
|
const fullVideo = video || getMessageWebPageVideo(message);
|
||||||
|
const size = (video || audio || document)?.size!;
|
||||||
|
if (target === 'download' && IS_PROGRESSIVE_SUPPORTED && size > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED) {
|
||||||
|
return ApiMediaFormat.DownloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
if (fullVideo && IS_PROGRESSIVE_SUPPORTED && (
|
if (fullVideo && IS_PROGRESSIVE_SUPPORTED && (
|
||||||
target === 'viewerFull' || target === 'inline'
|
target === 'viewerFull' || target === 'inline'
|
||||||
)) {
|
)) {
|
||||||
return ApiMediaFormat.Progressive;
|
return ApiMediaFormat.Progressive;
|
||||||
} else if (audio || voice) {
|
}
|
||||||
|
|
||||||
|
if (audio || voice) {
|
||||||
// Safari
|
// Safari
|
||||||
if (voice && !IS_OPUS_SUPPORTED) {
|
if (voice && !IS_OPUS_SUPPORTED) {
|
||||||
return ApiMediaFormat.BlobUrl;
|
return ApiMediaFormat.BlobUrl;
|
||||||
|
|||||||
@ -320,12 +320,35 @@ function getInputLocation(location) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the appropriated part size when uploading or downloading files,
|
* Gets the appropriated part size when downloading files,
|
||||||
* given an initial file size.
|
* given an initial file size.
|
||||||
* @param fileSize
|
* @param fileSize
|
||||||
* @returns {Number}
|
* @returns {Number}
|
||||||
*/
|
*/
|
||||||
function getAppropriatedPartSize(fileSize) {
|
function getDownloadPartSize(fileSize) {
|
||||||
|
if (fileSize <= 104857600) { // 100MB
|
||||||
|
return 128;
|
||||||
|
}
|
||||||
|
if (fileSize <= 786432000) { // 750MB
|
||||||
|
return 256;
|
||||||
|
}
|
||||||
|
if (fileSize <= 2097152000) { // 2000MB
|
||||||
|
return 512;
|
||||||
|
}
|
||||||
|
if (fileSize <= 4194304000) { // 4000MB
|
||||||
|
return 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('File size too large');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriated part size when uploading files,
|
||||||
|
* given an initial file size.
|
||||||
|
* @param fileSize
|
||||||
|
* @returns {Number}
|
||||||
|
*/
|
||||||
|
function getUploadPartSize(fileSize) {
|
||||||
if (fileSize <= 104857600) { // 100MB
|
if (fileSize <= 104857600) { // 100MB
|
||||||
return 128;
|
return 128;
|
||||||
}
|
}
|
||||||
@ -683,7 +706,8 @@ module.exports = {
|
|||||||
getDisplayName,
|
getDisplayName,
|
||||||
// resolveId,
|
// resolveId,
|
||||||
// isListLike,
|
// isListLike,
|
||||||
getAppropriatedPartSize,
|
getDownloadPartSize,
|
||||||
|
getUploadPartSize,
|
||||||
// getInputLocation,
|
// getInputLocation,
|
||||||
strippedPhotoToJpg,
|
strippedPhotoToJpg,
|
||||||
getDC,
|
getDC,
|
||||||
|
|||||||
10
src/lib/gramjs/client/TelegramClient.d.ts
vendored
10
src/lib/gramjs/client/TelegramClient.d.ts
vendored
@ -1,9 +1,9 @@
|
|||||||
import { Api } from '..';
|
import type { Api } from '..';
|
||||||
|
|
||||||
import { BotAuthParams, UserAuthParams } from './auth';
|
import type { BotAuthParams, UserAuthParams } from './auth';
|
||||||
import { uploadFile, UploadFileParams } from './uploadFile';
|
import type { uploadFile, UploadFileParams } from './uploadFile';
|
||||||
import { downloadFile, DownloadFileParams } from './downloadFile';
|
import type { downloadFile, DownloadFileParams } from './downloadFile';
|
||||||
import { TwoFaParams, updateTwoFaSettings } from './2fa';
|
import type { TwoFaParams, updateTwoFaSettings } from './2fa';
|
||||||
|
|
||||||
declare class TelegramClient {
|
declare class TelegramClient {
|
||||||
constructor(...args: any);
|
constructor(...args: any);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import BigInt from 'big-integer';
|
import BigInt from 'big-integer';
|
||||||
import Api from '../tl/api';
|
import Api from '../tl/api';
|
||||||
import type TelegramClient from './TelegramClient';
|
import type TelegramClient from './TelegramClient';
|
||||||
import { getAppropriatedPartSize } from '../Utils';
|
|
||||||
import { sleep, createDeferred } from '../Helpers';
|
import { sleep, createDeferred } from '../Helpers';
|
||||||
|
import { getDownloadPartSize } from '../Utils';
|
||||||
import errors from '../errors';
|
import errors from '../errors';
|
||||||
|
|
||||||
interface OnProgress {
|
interface OnProgress {
|
||||||
@ -35,7 +35,6 @@ const MIN_CHUNK_SIZE = 4096;
|
|||||||
const DEFAULT_CHUNK_SIZE = 64; // kb
|
const DEFAULT_CHUNK_SIZE = 64; // kb
|
||||||
const ONE_MB = 1024 * 1024;
|
const ONE_MB = 1024 * 1024;
|
||||||
const DISCONNECT_SLEEP = 1000;
|
const DISCONNECT_SLEEP = 1000;
|
||||||
const MAX_BUFFER_SAFE_SIZE = 2000 * 1024 * 1024;
|
|
||||||
|
|
||||||
// when the sender requests hangs for 60 second we will reimport
|
// when the sender requests hangs for 60 second we will reimport
|
||||||
const SENDER_TIMEOUT = 60 * 1000;
|
const SENDER_TIMEOUT = 60 * 1000;
|
||||||
@ -83,7 +82,8 @@ class FileView {
|
|||||||
|
|
||||||
constructor(size?: number) {
|
constructor(size?: number) {
|
||||||
this.size = size;
|
this.size = size;
|
||||||
this.type = (size && size > MAX_BUFFER_SAFE_SIZE) ? 'opfs' : 'memory';
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
this.type = (size && size > (self as any).maxBufferSize) ? 'opfs' : 'memory';
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -158,7 +158,7 @@ async function downloadFile2(
|
|||||||
end = end && end < fileSize ? end : fileSize - 1;
|
end = end && end < fileSize ? end : fileSize - 1;
|
||||||
|
|
||||||
if (!partSizeKb) {
|
if (!partSizeKb) {
|
||||||
partSizeKb = fileSize ? getAppropriatedPartSize(fileSize) : DEFAULT_CHUNK_SIZE;
|
partSizeKb = fileSize ? getDownloadPartSize(fileSize) : DEFAULT_CHUNK_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const partSize = partSizeKb * 1024;
|
const partSize = partSizeKb * 1024;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { default as Api } from '../tl/api';
|
|||||||
|
|
||||||
import type TelegramClient from './TelegramClient';
|
import type TelegramClient from './TelegramClient';
|
||||||
import { generateRandomBytes, readBigIntFromBuffer, sleep } from '../Helpers';
|
import { generateRandomBytes, readBigIntFromBuffer, sleep } from '../Helpers';
|
||||||
import { getAppropriatedPartSize } from '../Utils';
|
import { getUploadPartSize } from '../Utils';
|
||||||
import errors from '../errors';
|
import errors from '../errors';
|
||||||
|
|
||||||
interface OnProgress {
|
interface OnProgress {
|
||||||
@ -34,7 +34,7 @@ export async function uploadFile(
|
|||||||
const fileId = readBigIntFromBuffer(generateRandomBytes(8), true, true);
|
const fileId = readBigIntFromBuffer(generateRandomBytes(8), true, true);
|
||||||
const isLarge = size > LARGE_FILE_THRESHOLD;
|
const isLarge = size > LARGE_FILE_THRESHOLD;
|
||||||
|
|
||||||
const partSize = getAppropriatedPartSize(size) * KB_TO_BYTES;
|
const partSize = getUploadPartSize(size) * KB_TO_BYTES;
|
||||||
const partCount = Math.floor((size + partSize - 1) / partSize);
|
const partCount = Math.floor((size + partSize - 1) / partSize);
|
||||||
|
|
||||||
// Make sure a new sender can be created before starting upload
|
// Make sure a new sender can be created before starting upload
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { DEBUG } from './config';
|
import { DEBUG } from './config';
|
||||||
import { respondForProgressive } from './serviceWorker/progressive';
|
import { respondForProgressive } from './serviceWorker/progressive';
|
||||||
|
import { respondForDownload } from './serviceWorker/download';
|
||||||
import { respondWithCache, clearAssetCache } from './serviceWorker/assetCache';
|
import { respondWithCache, clearAssetCache } from './serviceWorker/assetCache';
|
||||||
import { handlePush, handleNotificationClick, handleClientMessage } from './serviceWorker/pushNotification';
|
import { handlePush, handleNotificationClick, handleClientMessage } from './serviceWorker/pushNotification';
|
||||||
import { pause } from './util/schedulers';
|
import { pause } from './util/schedulers';
|
||||||
@ -38,7 +39,6 @@ self.addEventListener('activate', (e) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals
|
|
||||||
self.addEventListener('fetch', (e: FetchEvent) => {
|
self.addEventListener('fetch', (e: FetchEvent) => {
|
||||||
const { url } = e.request;
|
const { url } = e.request;
|
||||||
|
|
||||||
@ -47,6 +47,11 @@ self.addEventListener('fetch', (e: FetchEvent) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.includes('/download/')) {
|
||||||
|
e.respondWith(respondForDownload(e));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.startsWith('http') && url.match(ASSET_CACHE_PATTERN)) {
|
if (url.startsWith('http') && url.match(ASSET_CACHE_PATTERN)) {
|
||||||
e.respondWith(respondWithCache(e));
|
e.respondWith(respondWithCache(e));
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
99
src/serviceWorker/download.ts
Normal file
99
src/serviceWorker/download.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { DEBUG } from '../config';
|
||||||
|
import { requestPart } from './progressive';
|
||||||
|
|
||||||
|
const DOWNLOAD_PART_SIZE = 1024 * 1024;
|
||||||
|
const TEST_PART_SIZE = 64 * 1024;
|
||||||
|
|
||||||
|
const QUEUE_SIZE = 5;
|
||||||
|
|
||||||
|
class FilePartQueue<T> {
|
||||||
|
queue: Promise<T>[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
push(task: Promise<T>) {
|
||||||
|
this.queue.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pop(): Promise<T> {
|
||||||
|
const result = await this.queue.shift()!;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.queue.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function respondForDownload(e: FetchEvent) {
|
||||||
|
const { url } = e.request;
|
||||||
|
|
||||||
|
let partInfo;
|
||||||
|
try {
|
||||||
|
partInfo = await requestPart(e, { url, start: 0, end: TEST_PART_SIZE });
|
||||||
|
} catch (err) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('FETCH DOWNLOAD', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!partInfo) {
|
||||||
|
return new Response('', {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Failed to fetch file to download',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedFilename = e.request.url.match(/filename=(.*)/);
|
||||||
|
const filenameHeader = matchedFilename ? `filename="${decodeURIComponent(matchedFilename[1])}"` : '';
|
||||||
|
const { fullSize, mimeType } = partInfo;
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
['Content-Length', String(fullSize)],
|
||||||
|
['Content-Type', mimeType],
|
||||||
|
['Content-Disposition', `attachment; ${filenameHeader}`],
|
||||||
|
];
|
||||||
|
|
||||||
|
const queue = new FilePartQueue<ArrayBuffer | undefined>();
|
||||||
|
const enqueue = (offset: number) => {
|
||||||
|
queue.push(requestPart(e, { url, start: offset, end: offset + DOWNLOAD_PART_SIZE - 1 })
|
||||||
|
.then((part) => part?.arrayBuffer));
|
||||||
|
return offset + DOWNLOAD_PART_SIZE;
|
||||||
|
};
|
||||||
|
let lastOffset = 0;
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start() {
|
||||||
|
for (let i = 0; i < QUEUE_SIZE; i++) {
|
||||||
|
if (lastOffset >= fullSize) break;
|
||||||
|
lastOffset = enqueue(lastOffset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async pull(controller) {
|
||||||
|
const buffer = await queue.pop();
|
||||||
|
if (!buffer) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.enqueue(new Uint8Array(buffer));
|
||||||
|
|
||||||
|
if (buffer.byteLength < DOWNLOAD_PART_SIZE) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastOffset < fullSize) {
|
||||||
|
lastOffset = enqueue(lastOffset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -137,16 +137,14 @@ async function saveToCache(cacheKey: string, arrayBuffer: ArrayBuffer, headers:
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestPart(
|
export async function requestPart(
|
||||||
e: FetchEvent,
|
e: FetchEvent,
|
||||||
params: { url: string; start: number; end: number },
|
params: { url: string; start: number; end: number },
|
||||||
): Promise<PartInfo | undefined> {
|
): Promise<PartInfo | undefined> {
|
||||||
if (!e.clientId) {
|
const isDownload = params.url.includes('/download/');
|
||||||
return undefined;
|
const client = isDownload ? (await self.clients.matchAll())
|
||||||
}
|
.find((c) => c.type === 'window' && c.frameType === 'top-level')
|
||||||
|
: await (self.clients.get(e.clientId));
|
||||||
// eslint-disable-next-line no-restricted-globals
|
|
||||||
const client = await self.clients.get(e.clientId);
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,6 +114,8 @@ export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
|||||||
// Smaller area reduces scroll jumps caused by `patchChromiumScroll`
|
// Smaller area reduces scroll jumps caused by `patchChromiumScroll`
|
||||||
export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750;
|
export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750;
|
||||||
|
|
||||||
|
export const MAX_BUFFER_SIZE = (IS_ANDROID || IS_IOS ? 512 : 2000) * 1024 ** 2; // 512 OR 2000 MB
|
||||||
|
|
||||||
function isLastEmojiVersionSupported() {
|
function isLastEmojiVersionSupported() {
|
||||||
const ALLOWABLE_CALCULATION_ERROR_SIZE = 5;
|
const ALLOWABLE_CALCULATION_ERROR_SIZE = 5;
|
||||||
const inlineEl = document.createElement('span');
|
const inlineEl = document.createElement('span');
|
||||||
|
|||||||
@ -13,18 +13,22 @@ import {
|
|||||||
import { callApi, cancelApiProgress } from '../api/gramjs';
|
import { callApi, cancelApiProgress } from '../api/gramjs';
|
||||||
import * as cacheApi from './cacheApi';
|
import * as cacheApi from './cacheApi';
|
||||||
import { fetchBlob } from './files';
|
import { fetchBlob } from './files';
|
||||||
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, isWebpSupported } from './environment';
|
import {
|
||||||
|
IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, isWebpSupported,
|
||||||
|
} from './environment';
|
||||||
import { oggToWav } from './oggToWav';
|
import { oggToWav } from './oggToWav';
|
||||||
import { webpToPng } from './webpToPng';
|
import { webpToPng } from './webpToPng';
|
||||||
|
|
||||||
const asCacheApiType = {
|
const asCacheApiType = {
|
||||||
[ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob,
|
[ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob,
|
||||||
[ApiMediaFormat.Text]: cacheApi.Type.Text,
|
[ApiMediaFormat.Text]: cacheApi.Type.Text,
|
||||||
|
[ApiMediaFormat.DownloadUrl]: undefined,
|
||||||
[ApiMediaFormat.Progressive]: undefined,
|
[ApiMediaFormat.Progressive]: undefined,
|
||||||
[ApiMediaFormat.Stream]: undefined,
|
[ApiMediaFormat.Stream]: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROGRESSIVE_URL_PREFIX = './progressive/';
|
const PROGRESSIVE_URL_PREFIX = './progressive/';
|
||||||
|
const URL_DOWNLOAD_PREFIX = './download/';
|
||||||
|
|
||||||
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>>();
|
||||||
@ -46,6 +50,14 @@ export function fetch<T extends ApiMediaFormat>(
|
|||||||
) as Promise<ApiPreparedMedia>;
|
) as Promise<ApiPreparedMedia>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mediaFormat === ApiMediaFormat.DownloadUrl) {
|
||||||
|
return (
|
||||||
|
IS_PROGRESSIVE_SUPPORTED
|
||||||
|
? getDownloadUrl(url)
|
||||||
|
: fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress, callbackUniqueId)
|
||||||
|
) as Promise<ApiPreparedMedia>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!fetchPromises.has(url)) {
|
if (!fetchPromises.has(url)) {
|
||||||
const promise = fetchFromCacheOrRemote(url, mediaFormat, isHtmlAllowed)
|
const promise = fetchFromCacheOrRemote(url, mediaFormat, isHtmlAllowed)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -110,6 +122,10 @@ function getProgressive(url: string) {
|
|||||||
return Promise.resolve(progressiveUrl);
|
return Promise.resolve(progressiveUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDownloadUrl(url: string) {
|
||||||
|
return Promise.resolve(`${URL_DOWNLOAD_PREFIX}${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFromCacheOrRemote(
|
async function fetchFromCacheOrRemote(
|
||||||
url: string, mediaFormat: ApiMediaFormat, isHtmlAllowed: boolean,
|
url: string, mediaFormat: ApiMediaFormat, isHtmlAllowed: boolean,
|
||||||
) {
|
) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user