From 96670d0454e062e492ec4d047ad43bad207875fc Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 15 May 2023 10:57:33 +0200 Subject: [PATCH] Message: Fix downloading scheduled media (#3174) --- src/api/gramjs/apiBuilders/messages.ts | 2 + src/components/common/Audio.tsx | 2 +- src/components/common/Document.tsx | 2 +- src/components/left/search/AudioResults.tsx | 2 +- src/components/left/search/FileResults.tsx | 2 +- .../search/helpers/createMapStateToProps.ts | 4 +- src/components/main/DownloadManager.tsx | 21 +++++--- .../mediaViewer/MediaViewerActions.tsx | 2 +- src/components/middle/message/Album.tsx | 14 +++--- .../middle/message/ContextMenuContainer.tsx | 19 ++++--- src/components/middle/message/Message.tsx | 2 +- src/components/middle/message/Photo.tsx | 2 +- src/components/middle/message/Video.tsx | 2 +- src/components/right/Profile.tsx | 14 +++--- src/global/actions/ui/messages.ts | 44 ++++------------ src/global/helpers/messages.ts | 2 +- src/global/reducers/messages.ts | 50 +++++++++++++++++++ src/global/selectors/messages.ts | 9 ++-- src/global/types.ts | 7 ++- 19 files changed, 126 insertions(+), 76 deletions(-) diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index d4bc07aad..c0af317c1 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -185,6 +185,7 @@ export function buildApiMessageWithChatId( if (action) { content.action = action; } + const isScheduled = mtpMessage.date > (Math.round(Date.now() / 1000) + getServerTimeOffset()); const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice && Boolean(mtpMessage.media.extendedMedia); @@ -216,6 +217,7 @@ export function buildApiMessageWithChatId( senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId, views: mtpMessage.views, forwards: mtpMessage.forwards, + isScheduled, isFromScheduled: mtpMessage.fromScheduled, isSilent: mtpMessage.silent, isPinned: mtpMessage.pinned, diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index c7ec9efda..046532b16 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -53,7 +53,7 @@ type OwnProps = { className?: string; isSelectable?: boolean; isSelected?: boolean; - isDownloading: boolean; + isDownloading?: boolean; isTranscribing?: boolean; isTranscribed?: boolean; canDownload?: boolean; diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 66b974359..6b6aea582 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -39,7 +39,7 @@ type OwnProps = { className?: string; sender?: string; autoLoadFileMaxSizeMb?: number; - isDownloading: boolean; + isDownloading?: boolean; onCancelUpload?: () => void; onMediaClick?: () => void; onDateClick?: (messageId: number, chatId: string) => void; diff --git a/src/components/left/search/AudioResults.tsx b/src/components/left/search/AudioResults.tsx index f541c8d6e..9b73c4de9 100644 --- a/src/components/left/search/AudioResults.tsx +++ b/src/components/left/search/AudioResults.tsx @@ -103,7 +103,7 @@ const AudioResults: FC = ({ onPlay={handlePlayAudio} onDateClick={handleMessageFocus} canDownload={!chatsById[message.chatId]?.isProtected && !message.isProtected} - isDownloading={activeDownloads[message.chatId]?.includes(message.id)} + isDownloading={activeDownloads[message.chatId]?.ids?.includes(message.id)} /> ); diff --git a/src/components/left/search/FileResults.tsx b/src/components/left/search/FileResults.tsx index bd54926ac..a336678ff 100644 --- a/src/components/left/search/FileResults.tsx +++ b/src/components/left/search/FileResults.tsx @@ -105,7 +105,7 @@ const FileResults: FC = ({ smaller sender={getSenderName(lang, message, chatsById, usersById)} className="scroll-item" - isDownloading={activeDownloads[message.chatId]?.includes(message.id)} + isDownloading={activeDownloads[message.chatId]?.ids?.includes(message.id)} observeIntersection={observeIntersectionForMedia} onDateClick={handleMessageFocus} /> diff --git a/src/components/left/search/helpers/createMapStateToProps.ts b/src/components/left/search/helpers/createMapStateToProps.ts index f310b5240..f769e6f80 100644 --- a/src/components/left/search/helpers/createMapStateToProps.ts +++ b/src/components/left/search/helpers/createMapStateToProps.ts @@ -1,4 +1,4 @@ -import type { GlobalState } from '../../../../global/types'; +import type { GlobalState, TabState } from '../../../../global/types'; import type { ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiUser, } from '../../../../api/types'; @@ -15,7 +15,7 @@ export type StateProps = { foundIds?: string[]; lastSyncTime?: number; searchChatId?: string; - activeDownloads: Record; + activeDownloads: TabState['activeDownloads']['byChatId']; isChatProtected?: boolean; }; diff --git a/src/components/main/DownloadManager.tsx b/src/components/main/DownloadManager.tsx index c37d6b675..e252240a6 100644 --- a/src/components/main/DownloadManager.tsx +++ b/src/components/main/DownloadManager.tsx @@ -1,6 +1,6 @@ import type { FC } from '../../lib/teact/teact'; import { memo, useCallback, useEffect } from '../../lib/teact/teact'; -import { getActions, withGlobal } from '../../global'; +import { getActions, getGlobal, withGlobal } from '../../global'; import type { GlobalState, TabState } from '../../global/types'; import type { ApiMessage } from '../../api/types'; @@ -13,6 +13,7 @@ import download from '../../util/download'; import { getMessageContentFilename, getMessageMediaFormat, getMessageMediaHash, } from '../../global/helpers'; +import { compact } from '../../util/iteratees'; import useRunDebounced from '../../hooks/useRunDebounced'; @@ -28,7 +29,6 @@ const downloadedMessages = new Set(); const DownloadManager: FC = ({ activeDownloads, - messages, }) => { const { cancelMessagesMediaDownload, showNotification } = getActions(); @@ -45,9 +45,16 @@ const DownloadManager: FC = ({ }, [cancelMessagesMediaDownload, runDebounced]); useEffect(() => { - const activeMessages = Object.entries(activeDownloads).map(([chatId, messageIds]) => ( - messageIds.map((id) => messages![chatId].byId[id]) - )).flat(); + // No need for expensive global updates on messages, so we avoid them + const messages = getGlobal().messages.byChatId; + const scheduledMessages = getGlobal().scheduledMessages.byChatId; + + const activeMessages = Object.entries(activeDownloads).map(([chatId, chatActiveDownloads]) => { + const chatMessages = chatActiveDownloads.ids?.map((id) => messages[chatId]?.byId[id]); + const chatScheduledMessages = chatActiveDownloads.scheduledIds?.map((id) => scheduledMessages[chatId]?.byId[id]); + + return compact([...chatMessages || [], ...chatScheduledMessages || []]); + }).flat(); if (!activeMessages.length) { processedMessages.clear(); @@ -104,7 +111,7 @@ const DownloadManager: FC = ({ handleMessageDownloaded(message); }); }); - }, [messages, activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded, showNotification]); + }, [activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded, showNotification]); return undefined; }; @@ -112,11 +119,9 @@ const DownloadManager: FC = ({ export default memo(withGlobal( (global): StateProps => { const activeDownloads = selectTabState(global).activeDownloads.byChatId; - const hasActiveDownloads = Object.values(activeDownloads).some((messageIds) => messageIds.length); return { activeDownloads, - messages: hasActiveDownloads ? global.messages.byChatId : undefined, }; }, )(DownloadManager)); diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index d1acdec20..a27c0c490 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -37,7 +37,7 @@ import DeleteProfilePhotoModal from '../common/DeleteProfilePhotoModal'; import './MediaViewerActions.scss'; type StateProps = { - isDownloading: boolean; + isDownloading?: boolean; isProtected?: boolean; isChatProtected?: boolean; canDelete?: boolean; diff --git a/src/components/middle/message/Album.tsx b/src/components/middle/message/Album.tsx index a5d089fde..ad37118f0 100644 --- a/src/components/middle/message/Album.tsx +++ b/src/components/middle/message/Album.tsx @@ -12,7 +12,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global'; import withSelectControl from './hocs/withSelectControl'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { - selectActiveDownloadIds, + selectActiveDownloads, selectCanAutoLoadMedia, selectCanAutoPlayMedia, selectTheme, @@ -40,7 +40,7 @@ type OwnProps = { type StateProps = { theme: ISettings['theme']; uploadsById: GlobalState['fileUploads']['byMessageLocalId']; - activeDownloadIds: number[]; + activeDownloadIds?: number[]; }; const Album: FC = ({ @@ -92,7 +92,7 @@ const Album: FC = ({ isProtected={isProtected} onClick={onMediaClick} onCancelUpload={handleCancelUpload} - isDownloading={activeDownloadIds.includes(message.id)} + isDownloading={activeDownloadIds?.includes(message.id)} theme={theme} /> ); @@ -110,7 +110,7 @@ const Album: FC = ({ isProtected={isProtected} onClick={onMediaClick} onCancelUpload={handleCancelUpload} - isDownloading={activeDownloadIds.includes(message.id)} + isDownloading={activeDownloadIds?.includes(message.id)} theme={theme} /> ); @@ -135,11 +135,13 @@ export default withGlobal( (global, { album }): StateProps => { const { chatId } = album.mainMessage; const theme = selectTheme(global); - const activeDownloadIds = selectActiveDownloadIds(global, chatId); + const activeDownloads = selectActiveDownloads(global, chatId); + const isScheduled = album.mainMessage.isScheduled; + return { theme, uploadsById: global.fileUploads.byMessageLocalId, - activeDownloadIds, + activeDownloadIds: isScheduled ? activeDownloads?.scheduledIds : activeDownloads?.ids, }; }, )(Album); diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 1d8f7ed46..e6bcf24db 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -4,14 +4,14 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; -import type { MessageListType } from '../../../global/types'; +import type { MessageListType, TabState } from '../../../global/types'; import type { ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet, ApiChatReactions, ApiReaction, ApiThreadInfo, } from '../../../api/types'; import type { IAlbum, IAnchorPosition } from '../../../types'; import { - selectActiveDownloadIds, + selectActiveDownloads, selectAllowedMessageActions, selectCanPlayAnimatedEmojis, selectCanScheduleUntilOnline, @@ -101,7 +101,7 @@ type StateProps = { canSaveGif?: boolean; canRevote?: boolean; canClosePoll?: boolean; - activeDownloads: number[]; + activeDownloads?: TabState['activeDownloads']['byChatId'][number]; canShowSeenBy?: boolean; enabledReactions?: ApiChatReactions; canScheduleUntilOnline?: boolean; @@ -249,8 +249,15 @@ const ContextMenuContainer: FC = ({ return Object.keys(message.seenByDates).slice(0, 3).map((id) => usersById[id]).filter(Boolean); }, [message.reactions?.recentReactions, message.seenByDates]); - const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id)) - : activeDownloads.includes(message.id); + const isDownloading = useMemo(() => { + if (album) { + return album.messages.some((msg) => { + return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(msg.id); + }); + } + + return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(message.id); + }, [activeDownloads, album, message]); const handleDelete = useCallback(() => { setIsMenuOpen(false); @@ -566,7 +573,7 @@ const ContextMenuContainer: FC = ({ export default memo(withGlobal( (global, { message, messageListType, detectedLanguage }): StateProps => { const { threadId } = selectCurrentMessageList(global) || {}; - const activeDownloads = selectActiveDownloadIds(global, message.chatId); + const activeDownloads = selectActiveDownloads(global, message.chatId); const chat = selectChat(global, message.chatId); const { seenByExpiresAt, seenByMaxChatMembers, maxUniqueReactions } = global.appConfig || {}; const { diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 3e85dfbea..92bcf93d8 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -228,7 +228,7 @@ type StateProps = { isInSelectMode?: boolean; isSelected?: boolean; isGroupSelected?: boolean; - isDownloading: boolean; + isDownloading?: boolean; threadId?: number; isPinnedList?: boolean; isPinned?: boolean; diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index fc6a4f19d..eb990b16d 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -51,7 +51,7 @@ export type OwnProps = { dimensions?: IMediaDimensions & { isSmall?: boolean }; asForwarded?: boolean; nonInteractive?: boolean; - isDownloading: boolean; + isDownloading?: boolean; isProtected?: boolean; theme: ISettings['theme']; onClick?: (id: number) => void; diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index 87fbcee2e..06443c8f5 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -47,7 +47,7 @@ export type OwnProps = { dimensions?: IMediaDimensions; asForwarded?: boolean; lastSyncTime?: number; - isDownloading: boolean; + isDownloading?: boolean; isProtected?: boolean; onClick?: (id: number) => void; onCancelUpload?: (message: ApiMessage) => void; diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index d3ac10474..3c97576e2 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -28,7 +28,7 @@ import { getHasAdminRight, isChatAdmin, isChatChannel, isChatGroup, isUserBot, isUserId, isUserRightBanned, } from '../../global/helpers'; import { - selectActiveDownloadIds, + selectActiveDownloads, selectChat, selectChatFullInfo, selectChatMessages, @@ -97,7 +97,7 @@ type StateProps = { isRightColumnShown: boolean; isRestricted?: boolean; lastSyncTime?: number; - activeDownloadIds: number[]; + activeDownloadIds?: number[]; isChatProtected?: boolean; }; @@ -373,7 +373,7 @@ const Profile: FC = ({ withDate smaller className="scroll-item" - isDownloading={activeDownloadIds.includes(id)} + isDownloading={activeDownloadIds?.includes(id)} observeIntersection={observeIntersectionForMedia} onDateClick={handleMessageFocus} /> @@ -401,7 +401,7 @@ const Profile: FC = ({ onPlay={handlePlayAudio} onDateClick={handleMessageFocus} canDownload={!isChatProtected && !messagesById[id].isProtected} - isDownloading={activeDownloadIds.includes(id)} + isDownloading={activeDownloadIds?.includes(id)} /> )) ) : resultType === 'voice' ? ( @@ -418,7 +418,7 @@ const Profile: FC = ({ onPlay={handlePlayAudio} onDateClick={handleMessageFocus} canDownload={!isChatProtected && !messagesById[id].isProtected} - isDownloading={activeDownloadIds.includes(id)} + isDownloading={activeDownloadIds?.includes(id)} /> )) ) : resultType === 'members' ? ( @@ -537,7 +537,7 @@ export default memo(withGlobal( const canAddMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'inviteUsers') || !isUserRightBanned(chat, 'inviteUsers') || chat.isCreator); const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator); - const activeDownloadIds = selectActiveDownloadIds(global, chatId); + const activeDownloads = selectActiveDownloads(global, chatId); let hasCommonChatsTab; let resolvedUserId; @@ -564,7 +564,7 @@ export default memo(withGlobal( isRightColumnShown: selectIsRightColumnShown(global, isMobile), isRestricted: chat?.isRestricted, lastSyncTime: global.lastSyncTime, - activeDownloadIds, + activeDownloadIds: activeDownloads?.ids, usersById, userStatusesById, chatsById, diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index e5169a433..a3b2fec53 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -4,7 +4,7 @@ import type { ApiMessage } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { FocusDirection } from '../../../types'; import type { - TabState, GlobalState, ActionReturnType, + GlobalState, ActionReturnType, } from '../../types'; import { @@ -23,6 +23,8 @@ import { replaceTabThreadParam, updateFocusDirection, updateFocusedMessage, + cancelMessageMediaDownload, + addActiveMessageMediaDownload, } from '../../reducers'; import { selectCurrentChat, @@ -557,49 +559,23 @@ addActionHandler('openForwardMenuForSelectedMessages', (global, actions, payload addActionHandler('cancelMessageMediaDownload', (global, actions, payload): ActionReturnType => { const { message, tabId = getCurrentTabId() } = payload; - const tabState = selectTabState(global, tabId); - const byChatId = tabState.activeDownloads.byChatId[message.chatId]; - if (!byChatId || !byChatId.length) return; - - global = updateTabState(global, { - activeDownloads: { - byChatId: { - ...tabState.activeDownloads.byChatId, - [message.chatId]: byChatId.filter((id) => id !== message.id), - }, - }, - }, tabId); - setGlobal(global); + return cancelMessageMediaDownload(global, message, tabId); }); addActionHandler('cancelMessagesMediaDownload', (global, actions, payload): ActionReturnType => { const { messages, tabId = getCurrentTabId() } = payload; - const byChatId = selectTabState(global, tabId).activeDownloads.byChatId; - const newByChatId: TabState['activeDownloads']['byChatId'] = {}; - Object.keys(byChatId).forEach((chatId) => { - newByChatId[chatId] = byChatId[chatId].filter((id) => !messages.find((message) => message.id === id)); - }); - return updateTabState(global, { - activeDownloads: { - byChatId: newByChatId, - }, - }, tabId); + for (const message of messages) { + global = cancelMessageMediaDownload(global, message, tabId); + } + + return global; }); addActionHandler('downloadMessageMedia', (global, actions, payload): ActionReturnType => { const { message, tabId = getCurrentTabId() } = payload; - const tabState = selectTabState(global, tabId); - global = updateTabState(global, { - activeDownloads: { - byChatId: { - ...tabState.activeDownloads.byChatId, - [message.chatId]: [...(tabState.activeDownloads.byChatId[message.chatId] || []), message.id], - }, - }, - }, tabId); - setGlobal(global); + return addActiveMessageMediaDownload(global, message, tabId); }); addActionHandler('downloadSelectedMessages', (global, actions, payload): ActionReturnType => { diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index b4c1f0cd3..4c7fd8108 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -230,7 +230,7 @@ export function getMessageContentFilename(message: ApiMessage) { return content.audio.fileName; } - const baseFilename = getMessageKey(message); + const baseFilename = `${getMessageKey(message)}${message.isScheduled ? '_scheduled' : ''}`; if (photo) { return `${baseFilename}.jpg`; diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index 5573b0b96..4a4f3769e 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -691,3 +691,53 @@ export function updateTopicLastMessageId( }, }; } + +export function addActiveMessageMediaDownload( + global: T, + message: ApiMessage, + ...[tabId = getCurrentTabId()]: TabArgs +) { + const tabState = selectTabState(global, tabId); + const byChatId = tabState.activeDownloads.byChatId[message.chatId] || {}; + const currentIds = (message.isScheduled ? byChatId?.scheduledIds : byChatId?.ids) || []; + + global = updateTabState(global, { + activeDownloads: { + byChatId: { + ...tabState.activeDownloads.byChatId, + [message.chatId]: { + ...byChatId, + [message.isScheduled ? 'scheduledIds' : 'ids']: unique([...currentIds, message.id]), + }, + }, + }, + }, tabId); + + return global; +} + +export function cancelMessageMediaDownload( + global: T, + message: ApiMessage, + ...[tabId = getCurrentTabId()]: TabArgs +) { + const tabState = selectTabState(global, tabId); + const byChatId = tabState.activeDownloads.byChatId[message.chatId]; + if (!byChatId) return global; + + const currentIds = (message.isScheduled ? byChatId.scheduledIds : byChatId.ids) || []; + + global = updateTabState(global, { + activeDownloads: { + byChatId: { + ...tabState.activeDownloads.byChatId, + [message.chatId]: { + ...byChatId, + [message.isScheduled ? 'scheduledIds' : 'ids']: currentIds.filter((id) => id !== message.id), + }, + }, + }, + }, tabId); + + return global; +} diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 2d04c8635..6981171cd 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -725,14 +725,17 @@ export function selectIsDownloading( ...[tabId = getCurrentTabId()]: TabArgs ) { const activeInChat = selectTabState(global, tabId).activeDownloads.byChatId[message.chatId]; - return activeInChat ? activeInChat.includes(message.id) : false; + if (!activeInChat) return false; + + return Boolean(message.isScheduled + ? activeInChat.scheduledIds?.includes(message.id) : activeInChat.ids?.includes(message.id)); } -export function selectActiveDownloadIds( +export function selectActiveDownloads( global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs ) { - return selectTabState(global, tabId).activeDownloads.byChatId[chatId] || MEMO_EMPTY_ARRAY; + return selectTabState(global, tabId).activeDownloads.byChatId[chatId]; } export function selectUploadProgress(global: T, message: ApiMessage) { diff --git a/src/global/types.ts b/src/global/types.ts index 63660dca3..ffdffe789 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -440,7 +440,12 @@ export type TabState = { openedCustomEmojiSetIds?: string[]; activeDownloads: { - byChatId: Record; + byChatId: { + [chatId: string]: { + ids?: number[]; + scheduledIds?: number[]; + }; + }; }; statistics: {