Message: Fix downloading scheduled media (#3174)

This commit is contained in:
Alexander Zinchuk 2023-05-15 10:57:33 +02:00
parent a71a3bc53b
commit 96670d0454
19 changed files with 126 additions and 76 deletions

View File

@ -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,

View File

@ -53,7 +53,7 @@ type OwnProps = {
className?: string;
isSelectable?: boolean;
isSelected?: boolean;
isDownloading: boolean;
isDownloading?: boolean;
isTranscribing?: boolean;
isTranscribed?: boolean;
canDownload?: boolean;

View File

@ -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;

View File

@ -103,7 +103,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
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)}
/>
</div>
);

View File

@ -105,7 +105,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
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}
/>

View File

@ -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<string, number[]>;
activeDownloads: TabState['activeDownloads']['byChatId'];
isChatProtected?: boolean;
};

View File

@ -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<ApiMessage>();
const DownloadManager: FC<StateProps> = ({
activeDownloads,
messages,
}) => {
const { cancelMessagesMediaDownload, showNotification } = getActions();
@ -45,9 +45,16 @@ const DownloadManager: FC<StateProps> = ({
}, [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<StateProps> = ({
handleMessageDownloaded(message);
});
});
}, [messages, activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded, showNotification]);
}, [activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded, showNotification]);
return undefined;
};
@ -112,11 +119,9 @@ const DownloadManager: FC<StateProps> = ({
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));

View File

@ -37,7 +37,7 @@ import DeleteProfilePhotoModal from '../common/DeleteProfilePhotoModal';
import './MediaViewerActions.scss';
type StateProps = {
isDownloading: boolean;
isDownloading?: boolean;
isProtected?: boolean;
isChatProtected?: boolean;
canDelete?: boolean;

View File

@ -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<OwnProps & StateProps> = ({
@ -92,7 +92,7 @@ const Album: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps>(
(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);

View File

@ -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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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 {

View File

@ -228,7 +228,7 @@ type StateProps = {
isInSelectMode?: boolean;
isSelected?: boolean;
isGroupSelected?: boolean;
isDownloading: boolean;
isDownloading?: boolean;
threadId?: number;
isPinnedList?: boolean;
isPinned?: boolean;

View File

@ -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;

View File

@ -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;

View File

@ -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<OwnProps & StateProps> = ({
withDate
smaller
className="scroll-item"
isDownloading={activeDownloadIds.includes(id)}
isDownloading={activeDownloadIds?.includes(id)}
observeIntersection={observeIntersectionForMedia}
onDateClick={handleMessageFocus}
/>
@ -401,7 +401,7 @@ const Profile: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps>(
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<OwnProps>(
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isRestricted: chat?.isRestricted,
lastSyncTime: global.lastSyncTime,
activeDownloadIds,
activeDownloadIds: activeDownloads?.ids,
usersById,
userStatusesById,
chatsById,

View File

@ -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 => {

View File

@ -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`;

View File

@ -691,3 +691,53 @@ export function updateTopicLastMessageId<T extends GlobalState>(
},
};
}
export function addActiveMessageMediaDownload<T extends GlobalState>(
global: T,
message: ApiMessage,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
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<T extends GlobalState>(
global: T,
message: ApiMessage,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
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;
}

View File

@ -725,14 +725,17 @@ export function selectIsDownloading<T extends GlobalState>(
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
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<T extends GlobalState>(
export function selectActiveDownloads<T extends GlobalState>(
global: T, chatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
return selectTabState(global, tabId).activeDownloads.byChatId[chatId] || MEMO_EMPTY_ARRAY;
return selectTabState(global, tabId).activeDownloads.byChatId[chatId];
}
export function selectUploadProgress<T extends GlobalState>(global: T, message: ApiMessage) {

View File

@ -440,7 +440,12 @@ export type TabState = {
openedCustomEmojiSetIds?: string[];
activeDownloads: {
byChatId: Record<string, number[]>;
byChatId: {
[chatId: string]: {
ids?: number[];
scheduledIds?: number[];
};
};
};
statistics: {