Message List: Fix duplicated downloads (#1781)
This commit is contained in:
parent
e2ef55f91c
commit
d4629cca8c
@ -1,4 +1,6 @@
|
||||
import { FC, memo, useEffect } from '../../lib/teact/teact';
|
||||
import {
|
||||
FC, memo, useCallback, useEffect,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import { Thread } from '../../global/types';
|
||||
@ -10,58 +12,76 @@ import {
|
||||
getMessageContentFilename, getMessageMediaHash,
|
||||
} from '../../global/helpers';
|
||||
|
||||
import useDebounce from '../../hooks/useDebounce';
|
||||
|
||||
type StateProps = {
|
||||
activeDownloads: Record<number, number[]>;
|
||||
messages: Record<number, {
|
||||
activeDownloads: Record<string, number[]>;
|
||||
messages: Record<string, {
|
||||
byId: Record<number, ApiMessage>;
|
||||
threadsById: Record<number, Thread>;
|
||||
}>;
|
||||
};
|
||||
|
||||
const startedDownloads = new Set<string>();
|
||||
const GLOBAL_UPDATE_DEBOUNCE = 1000;
|
||||
|
||||
const processedMessages = new Set<ApiMessage>();
|
||||
const downloadedMessages = new Set<ApiMessage>();
|
||||
|
||||
const DownloadManager: FC<StateProps> = ({
|
||||
activeDownloads,
|
||||
messages,
|
||||
}) => {
|
||||
const { cancelMessageMediaDownload } = getActions();
|
||||
const { cancelMessagesMediaDownload } = getActions();
|
||||
|
||||
const debouncedGlobalUpdate = useDebounce(GLOBAL_UPDATE_DEBOUNCE, true);
|
||||
|
||||
const handleMessageDownloaded = useCallback((message: ApiMessage) => {
|
||||
downloadedMessages.add(message);
|
||||
debouncedGlobalUpdate(() => {
|
||||
if (downloadedMessages.size) {
|
||||
cancelMessagesMediaDownload({ messages: Array.from(downloadedMessages) });
|
||||
downloadedMessages.clear();
|
||||
}
|
||||
});
|
||||
}, [cancelMessagesMediaDownload, debouncedGlobalUpdate]);
|
||||
|
||||
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(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);
|
||||
const activeMessages = Object.entries(activeDownloads).map(([chatId, messageIds]) => (
|
||||
messageIds.map((id) => messages[chatId].byId[id])
|
||||
)).flat();
|
||||
|
||||
if (!activeMessages.length) {
|
||||
processedMessages.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
activeMessages.forEach((message) => {
|
||||
if (processedMessages.has(message)) {
|
||||
return;
|
||||
}
|
||||
processedMessages.add(message);
|
||||
const downloadHash = getMessageMediaHash(message, 'download');
|
||||
if (!downloadHash) {
|
||||
handleMessageDownloaded(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaData = mediaLoader.getFromMemory(downloadHash);
|
||||
|
||||
if (mediaData) {
|
||||
download(mediaData, getMessageContentFilename(message));
|
||||
handleMessageDownloaded(message);
|
||||
return;
|
||||
}
|
||||
|
||||
mediaLoader.fetch(downloadHash, ApiMediaFormat.BlobUrl, true).then((result) => {
|
||||
if (result) {
|
||||
download(result, getMessageContentFilename(message));
|
||||
}
|
||||
handleMessageDownloaded(message);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
cancelMessageMediaDownload,
|
||||
messages,
|
||||
activeDownloads,
|
||||
]);
|
||||
}, [messages, activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded]);
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@ -63,9 +63,9 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleDownloadClick = useCallback(() => {
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message });
|
||||
cancelMessageMediaDownload({ message: message! });
|
||||
} else {
|
||||
downloadMessageMedia({ message });
|
||||
downloadMessageMedia({ message: message! });
|
||||
}
|
||||
}, [cancelMessageMediaDownload, downloadMessageMedia, isDownloading, message]);
|
||||
|
||||
|
||||
@ -460,7 +460,7 @@ addActionHandler('openForwardMenuForSelectedMessages', (global, actions) => {
|
||||
});
|
||||
|
||||
addActionHandler('cancelMessageMediaDownload', (global, actions, payload) => {
|
||||
const { message } = payload!;
|
||||
const { message } = payload;
|
||||
|
||||
const byChatId = global.activeDownloads.byChatId[message.chatId];
|
||||
if (!byChatId || !byChatId.length) return;
|
||||
@ -476,9 +476,24 @@ addActionHandler('cancelMessageMediaDownload', (global, actions, payload) => {
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('cancelMessagesMediaDownload', (global, actions, payload) => {
|
||||
const { messages } = payload;
|
||||
|
||||
const byChatId = global.activeDownloads.byChatId;
|
||||
const newByChatId: GlobalState['activeDownloads']['byChatId'] = {};
|
||||
Object.keys(byChatId).forEach((chatId) => {
|
||||
newByChatId[chatId] = byChatId[chatId].filter((id) => !messages.find((message) => message.id === id));
|
||||
});
|
||||
return {
|
||||
...global,
|
||||
activeDownloads: {
|
||||
byChatId: newByChatId,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('downloadMessageMedia', (global, actions, payload) => {
|
||||
const { message } = payload!;
|
||||
if (!message) return;
|
||||
const { message } = payload;
|
||||
|
||||
setGlobal({
|
||||
...global,
|
||||
|
||||
@ -585,6 +585,18 @@ export interface ActionPayloads {
|
||||
origin: AudioOrigin;
|
||||
};
|
||||
|
||||
// Downloads
|
||||
downloadSelectedMessages: {};
|
||||
downloadMessageMedia: {
|
||||
message: ApiMessage;
|
||||
};
|
||||
cancelMessageMediaDownload: {
|
||||
message: ApiMessage;
|
||||
};
|
||||
cancelMessagesMediaDownload: {
|
||||
messages: ApiMessage[];
|
||||
};
|
||||
|
||||
// Users
|
||||
openAddContactDialog: {
|
||||
userId?: string;
|
||||
@ -640,8 +652,6 @@ export type NonTypedActionNames = (
|
||||
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
|
||||
'stopActiveReaction' | 'startActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
||||
'setEditingId' |
|
||||
// downloads
|
||||
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||
// scheduled messages
|
||||
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
||||
// poll result
|
||||
|
||||
@ -1,4 +1,42 @@
|
||||
import { sleep } from '../lib/gramjs/Helpers';
|
||||
|
||||
type PendingDownload = {
|
||||
url: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
// Chrome prevents more than 10 downloads per second
|
||||
const LIMIT_PER_BATCH = 10;
|
||||
const BATCH_INTERVAL = 1000;
|
||||
|
||||
let pendingDownloads: PendingDownload[] = [];
|
||||
let planned = false;
|
||||
|
||||
export default function download(url: string, filename: string) {
|
||||
pendingDownloads.push({ url, filename });
|
||||
if (!planned) {
|
||||
planned = true;
|
||||
setTimeout(async () => {
|
||||
await processQueue();
|
||||
planned = false;
|
||||
}, BATCH_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
async function processQueue() {
|
||||
let count = 0;
|
||||
for (const pendingDownload of pendingDownloads) {
|
||||
downloadOne(pendingDownload);
|
||||
count++;
|
||||
if (count === LIMIT_PER_BATCH) {
|
||||
await sleep(BATCH_INTERVAL);
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
pendingDownloads = [];
|
||||
}
|
||||
|
||||
function downloadOne({ url, filename }: PendingDownload) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user