Message List: Fix duplicated downloads (#1781)

This commit is contained in:
Alexander Zinchuk 2022-04-01 20:43:16 +02:00
parent e2ef55f91c
commit d4629cca8c
5 changed files with 127 additions and 44 deletions

View File

@ -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 { getActions, withGlobal } from '../../global';
import { Thread } from '../../global/types'; import { Thread } from '../../global/types';
@ -10,58 +12,76 @@ import {
getMessageContentFilename, getMessageMediaHash, getMessageContentFilename, getMessageMediaHash,
} from '../../global/helpers'; } from '../../global/helpers';
import useDebounce from '../../hooks/useDebounce';
type StateProps = { type StateProps = {
activeDownloads: Record<number, number[]>; activeDownloads: Record<string, number[]>;
messages: Record<number, { messages: Record<string, {
byId: Record<number, ApiMessage>; byId: Record<number, ApiMessage>;
threadsById: Record<number, Thread>; 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> = ({ const DownloadManager: FC<StateProps> = ({
activeDownloads, activeDownloads,
messages, 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(() => { useEffect(() => {
Object.entries(activeDownloads).forEach(([chatId, messageIds]) => { const activeMessages = Object.entries(activeDownloads).map(([chatId, messageIds]) => (
const activeMessages = messageIds.map((id) => messages[Number(chatId)].byId[id]); messageIds.map((id) => messages[chatId].byId[id])
activeMessages.forEach((message) => { )).flat();
const downloadHash = getMessageMediaHash(message, 'download');
if (!downloadHash) { if (!activeMessages.length) {
cancelMessageMediaDownload({ message }); processedMessages.clear();
return; return;
} }
if (!startedDownloads.has(downloadHash)) { activeMessages.forEach((message) => {
const mediaData = mediaLoader.getFromMemory(downloadHash); if (processedMessages.has(message)) {
if (mediaData) { return;
startedDownloads.delete(downloadHash); }
download(mediaData, getMessageContentFilename(message)); processedMessages.add(message);
cancelMessageMediaDownload({ message }); const downloadHash = getMessageMediaHash(message, 'download');
return; if (!downloadHash) {
} handleMessageDownloaded(message);
return;
mediaLoader.fetch(downloadHash, ApiMediaFormat.BlobUrl, true).then((result) => { }
startedDownloads.delete(downloadHash);
if (result) { const mediaData = mediaLoader.getFromMemory(downloadHash);
download(result, getMessageContentFilename(message));
} if (mediaData) {
cancelMessageMediaDownload({ message }); download(mediaData, getMessageContentFilename(message));
}); handleMessageDownloaded(message);
return;
startedDownloads.add(downloadHash); }
mediaLoader.fetch(downloadHash, ApiMediaFormat.BlobUrl, true).then((result) => {
if (result) {
download(result, getMessageContentFilename(message));
} }
handleMessageDownloaded(message);
}); });
}); });
}, [ }, [messages, activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded]);
cancelMessageMediaDownload,
messages,
activeDownloads,
]);
return undefined; return undefined;
}; };

View File

@ -63,9 +63,9 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
const handleDownloadClick = useCallback(() => { const handleDownloadClick = useCallback(() => {
if (isDownloading) { if (isDownloading) {
cancelMessageMediaDownload({ message }); cancelMessageMediaDownload({ message: message! });
} else { } else {
downloadMessageMedia({ message }); downloadMessageMedia({ message: message! });
} }
}, [cancelMessageMediaDownload, downloadMessageMedia, isDownloading, message]); }, [cancelMessageMediaDownload, downloadMessageMedia, isDownloading, message]);

View File

@ -460,7 +460,7 @@ addActionHandler('openForwardMenuForSelectedMessages', (global, actions) => {
}); });
addActionHandler('cancelMessageMediaDownload', (global, actions, payload) => { addActionHandler('cancelMessageMediaDownload', (global, actions, payload) => {
const { message } = payload!; const { message } = payload;
const byChatId = global.activeDownloads.byChatId[message.chatId]; const byChatId = global.activeDownloads.byChatId[message.chatId];
if (!byChatId || !byChatId.length) return; 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) => { addActionHandler('downloadMessageMedia', (global, actions, payload) => {
const { message } = payload!; const { message } = payload;
if (!message) return;
setGlobal({ setGlobal({
...global, ...global,

View File

@ -585,6 +585,18 @@ export interface ActionPayloads {
origin: AudioOrigin; origin: AudioOrigin;
}; };
// Downloads
downloadSelectedMessages: {};
downloadMessageMedia: {
message: ApiMessage;
};
cancelMessageMediaDownload: {
message: ApiMessage;
};
cancelMessagesMediaDownload: {
messages: ApiMessage[];
};
// Users // Users
openAddContactDialog: { openAddContactDialog: {
userId?: string; userId?: string;
@ -640,8 +652,6 @@ export type NonTypedActionNames = (
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' | 'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
'stopActiveReaction' | 'startActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' | 'stopActiveReaction' | 'startActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
'setEditingId' | 'setEditingId' |
// downloads
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
// scheduled messages // scheduled messages
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' | 'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
// poll result // poll result

View File

@ -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) { 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'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = filename; link.download = filename;