diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 48d8c424ded6d3..cc216cb483f38d 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -2607,7 +2607,10 @@ bool SingleMessageAfter( const MTPmessages_Messages &data, TimeId date) { const auto single = SingleMessageDate(data); - return (single > 0 && single > date); + // Inclusive lower bound: SkipMessageByDate keeps messages with + // `singlePeerFrom <= date`, so a split whose newest message has the + // exact `singlePeerFrom` date must not be pre-skipped here. + return (single > 0 && single >= date); } bool SkipMessageByDate(const Message &message, const Settings &settings) { diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index f23c279f9d4236..ecb931919bdfea 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -258,6 +258,7 @@ struct ApiWrap::TopicProcess : AbstractMessagesProcess { int32 offsetId = 0; int totalCount = 0; int processedCount = 0; + bool firstSliceRequested = false; }; @@ -1464,6 +1465,20 @@ void ApiWrap::requestMessagesCount(int localSplitIndex) { Expects(_chatProcess != nullptr); Expects(localSplitIndex < _chatProcess->info.splits.size()); + // When the user picked a date range, ask the server for the count + // of messages matching `[singlePeerFrom, singlePeerTill)` in this + // split via messages.search. That gives the progress bar a real + // total for the filtered export (otherwise messagesCountPerSplit + // stores the full-chat size and progress effectively jumps from + // ~0% to 100% near the end). The same call also tells us whether + // the split is empty (count == 0) so it subsumes the previous + // SingleMessageAfter / SingleMessageBefore skip probes. + if ((_settings->singlePeerFrom > 0) + || (_settings->singlePeerTill > 0)) { + requestDateLimitedMessagesCount(localSplitIndex); + return; + } + requestChatMessages( _chatProcess->info.splits[localSplitIndex], 0, // offset_id @@ -1486,41 +1501,81 @@ void ApiWrap::requestMessagesCount(int localSplitIndex) { error("Unexpected messagesNotModified received."); return; } - const auto skipSplit = !Data::SingleMessageAfter( - result, - _settings->singlePeerFrom); - if (skipSplit) { - // No messages from the requested range, skip this split. - messagesCountLoaded(localSplitIndex, 0); - return; - } - checkFirstMessageDate(localSplitIndex, count); + messagesCountLoaded(localSplitIndex, count); }); } -void ApiWrap::checkFirstMessageDate(int localSplitIndex, int count) { +void ApiWrap::requestDateLimitedMessagesCount(int localSplitIndex) { Expects(_chatProcess != nullptr); Expects(localSplitIndex < _chatProcess->info.splits.size()); + Expects((_settings->singlePeerFrom > 0) + || (_settings->singlePeerTill > 0)); - if (_settings->singlePeerTill <= 0) { - messagesCountLoaded(localSplitIndex, count); - return; - } + const auto splitIndex = _chatProcess->info.splits[localSplitIndex]; + const auto splitsCount = int(_splits.size()); + const auto realSplitIndex = (splitIndex >= 0) + ? splitIndex + : (splitsCount + splitIndex); + const auto realPeerInput = (splitIndex >= 0) + ? _chatProcess->info.input + : _chatProcess->info.migratedFromInput; + const auto outgoingInput = _chatProcess->info.isMonoforum + ? _chatProcess->info.monoforumBroadcastInput + : MTP_inputPeerSelf(); - // Request first message in this split to check if its' date < till. - requestChatMessages( - _chatProcess->info.splits[localSplitIndex], - 1, // offset_id - -1, // add_offset - 1, // limit - [=](const MTPmessages_Messages &result) { + // See requestChatMessages: messages.search uses strict comparison + // for both bounds (date > min_date && date < max_date), while the + // exporter's canonical range is `[singlePeerFrom, singlePeerTill)` + // (SkipMessageByDate). Shift min_date by -1 to match. + const auto minDate = (_settings->singlePeerFrom > 0) + ? (_settings->singlePeerFrom - 1) + : 0; + const auto maxDate = (_settings->singlePeerTill > 0) + ? _settings->singlePeerTill + : 0; + const auto flags = _chatProcess->info.onlyMyMessages + ? MTPmessages_Search::Flag::f_from_id + : MTPmessages_Search::Flag(0); + const auto fromInput = _chatProcess->info.onlyMyMessages + ? outgoingInput + : MTPInputPeer(); + + splitRequest(realSplitIndex, MTPmessages_Search( + MTP_flags(flags), + realPeerInput, + MTP_string(), // query + fromInput, + MTPInputPeer(), // saved_peer_id + MTPVector(), // saved_reaction + MTPint(), // top_msg_id + MTP_inputMessagesFilterEmpty(), + MTP_int(minDate), // min_date + MTP_int(maxDate), // max_date + MTP_int(0), // offset_id + MTP_int(0), // add_offset + MTP_int(1), // limit - we only need .count + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { Expects(_chatProcess != nullptr); - const auto skipSplit = !Data::SingleMessageBefore( - result, - _settings->singlePeerTill); - messagesCountLoaded(localSplitIndex, skipSplit ? 0 : count); - }); + const auto count = result.match( + [](const MTPDmessages_messages &data) { + return int(data.vmessages().v.size()); + }, [](const MTPDmessages_messagesSlice &data) { + return data.vcount().v; + }, [](const MTPDmessages_channelMessages &data) { + return data.vcount().v; + }, [](const MTPDmessages_messagesNotModified &) { + return -1; + }); + if (count < 0) { + error("Unexpected messagesNotModified received."); + return; + } + messagesCountLoaded(localSplitIndex, count); + }).send(); } void ApiWrap::messagesCountLoaded(int localSplitIndex, int count) { @@ -1875,9 +1930,22 @@ void ApiWrap::requestMessagesSlice() { loadMessagesFiles({}); return; } + // If user picked a "from date" and this is the first slice of the split, + // jump to that date via MTP offset_date instead of walking the chat + // from the very beginning (id == 1). Otherwise we'd download every + // message in history just to skip it client-side. + const auto firstSlice = (_chatProcess->largestIdPlusOne <= 1); + const auto useDateOffset = firstSlice + && (_settings->singlePeerFrom > 0); + const auto offsetId = useDateOffset + ? 0 + : _chatProcess->largestIdPlusOne; + const auto offsetDate = useDateOffset + ? _settings->singlePeerFrom + : 0; requestChatMessages( _chatProcess->info.splits[_chatProcess->localSplitIndex], - _chatProcess->largestIdPlusOne, + offsetId, -kMessagesSliceLimit, kMessagesSliceLimit, [=](const MTPmessages_Messages &result) { @@ -1896,7 +1964,8 @@ void ApiWrap::requestMessagesSlice() { data.vchats(), _chatProcess->info.relativePath)); }); - }); + }, + offsetDate); } void ApiWrap::requestChatMessages( @@ -1904,7 +1973,8 @@ void ApiWrap::requestChatMessages( int offsetId, int addOffset, int limit, - FnMut done) { + FnMut done, + int offsetDate) { Expects(_chatProcess != nullptr); _chatProcess->requestDone = std::move(done); @@ -1923,6 +1993,19 @@ void ApiWrap::requestChatMessages( const auto realSplitIndex = (splitIndex >= 0) ? splitIndex : (splitsCount + splitIndex); + // Narrow server-side scan when the user requested a date range, so we + // don't pull every message in the chat just to skip them client-side. + // messages.search uses strict comparison for both bounds (date > + // min_date and date < max_date), while the exporter's canonical range + // is `[singlePeerFrom, singlePeerTill)` (see SkipMessageByDate). To + // match it exactly we shift min_date down by 1 and pass singlePeerTill + // through unchanged. + const auto minDate = (_settings->singlePeerFrom > 0) + ? (_settings->singlePeerFrom - 1) + : 0; + const auto maxDate = (_settings->singlePeerTill > 0) + ? _settings->singlePeerTill + : 0; if (_chatProcess->info.onlyMyMessages) { splitRequest(realSplitIndex, MTPmessages_Search( MTP_flags(MTPmessages_Search::Flag::f_from_id), @@ -1933,8 +2016,8 @@ void ApiWrap::requestChatMessages( MTPVector(), // saved_reaction MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), - MTP_int(0), // min_date - MTP_int(0), // max_date + MTP_int(minDate), // min_date + MTP_int(maxDate), // max_date MTP_int(offsetId), MTP_int(addOffset), MTP_int(limit), @@ -1946,7 +2029,7 @@ void ApiWrap::requestChatMessages( splitRequest(realSplitIndex, MTPmessages_GetHistory( realPeerInput, MTP_int(offsetId), - MTP_int(0), // offset_date + MTP_int(offsetDate), // offset_date MTP_int(addOffset), MTP_int(limit), MTP_int(0), // max_id @@ -1967,7 +2050,8 @@ void ApiWrap::requestChatMessages( offsetId, addOffset, limit, - base::take(_chatProcess->requestDone)); + base::take(_chatProcess->requestDone), + offsetDate); return true; } } @@ -2195,6 +2279,12 @@ void ApiWrap::finishMessagesSlice() { Expects(_chatProcess->slice.has_value()); auto slice = *base::take(_chatProcess->slice); + // If user picked an upper date bound and the newest message in this + // slice is already at/past it, the rest of the chat is also past it — + // no point in fetching further slices (used to walk to end of chat). + const auto reachedTill = (_settings->singlePeerTill > 0) + && !slice.list.empty() + && (slice.list.back().date >= _settings->singlePeerTill); if (!slice.list.empty()) { _chatProcess->largestIdPlusOne = slice.list.back().id + 1; const auto splitIndex = _chatProcess->info.splits[ @@ -2206,6 +2296,9 @@ void ApiWrap::finishMessagesSlice() { return; } } + if (reachedTill) { + _chatProcess->lastSlice = true; + } if (_chatProcess->lastSlice && (++_chatProcess->localSplitIndex < _chatProcess->info.splits.size())) { @@ -2414,9 +2507,24 @@ void ApiWrap::requestTopicMessages( void ApiWrap::requestTopicMessagesSlice() { Expects(_topicProcess != nullptr); - const auto offsetId = (_topicProcess->offsetId == 0) - ? 1 - : (_topicProcess->offsetId + 1); + // On the very first slice request for this topic, if the user picked + // a "from date", jump straight to it via MTP offset_date instead of + // walking the topic from its root id and skipping client-side. + // The initial root-message fetch leaves offsetId at the topic root id + // (or 0 if the root was empty), so we use an explicit "firstSlice" + // flag rather than overloading the offsetId == 0 check. + const auto firstSlice = !_topicProcess->firstSliceRequested; + _topicProcess->firstSliceRequested = true; + const auto useDateOffset = firstSlice + && (_settings->singlePeerFrom > 0); + const auto offsetId = useDateOffset + ? 0 + : ((_topicProcess->offsetId == 0) + ? 1 + : (_topicProcess->offsetId + 1)); + const auto offsetDate = useDateOffset + ? _settings->singlePeerFrom + : 0; requestTopicReplies( offsetId, -kMessagesSliceLimit, @@ -2441,14 +2549,16 @@ void ApiWrap::requestTopicMessagesSlice() { } loadTopicMessagesFiles(std::move(slice)); }); - }); + }, + offsetDate); } void ApiWrap::requestTopicReplies( int offsetId, int addOffset, int limit, - FnMut done) { + FnMut done, + int offsetDate) { Expects(_topicProcess != nullptr); _topicProcess->requestDone = std::move(done); @@ -2461,7 +2571,7 @@ void ApiWrap::requestTopicReplies( _topicProcess->inputPeer, MTP_int(_topicProcess->topicRootId), MTP_int(offsetId), - MTP_int(0), + MTP_int(offsetDate), // offset_date MTP_int(addOffset), MTP_int(limit), MTP_int(0), @@ -2552,6 +2662,9 @@ void ApiWrap::loadNextTopicMessageFile() { ; _topicProcess->fileIndex < list.size() ; ++_topicProcess->fileIndex) { auto &message = list[_topicProcess->fileIndex]; + if (Data::SkipMessageByDate(message, *_settings)) { + continue; + } if (!messageCustomEmojiReady(message)) { return; } @@ -2590,6 +2703,12 @@ void ApiWrap::finishTopicMessagesSlice() { Expects(_topicProcess->slice.has_value()); auto slice = *base::take(_topicProcess->slice); + // Same early-stop as for chat slices: if the newest message in this + // batch is already at/past the user-picked upper date bound, the rest + // of the topic is past it too — no need to keep walking. + const auto reachedTill = (_settings->singlePeerTill > 0) + && !slice.list.empty() + && (slice.list.back().date >= _settings->singlePeerTill); if (!slice.list.empty()) { _topicProcess->offsetId = slice.list.back().id; _topicProcess->processedCount += slice.list.size(); @@ -2601,7 +2720,7 @@ void ApiWrap::finishTopicMessagesSlice() { const auto reachedTotal = _topicProcess->totalCount > 0 && _topicProcess->processedCount >= _topicProcess->totalCount; - if (!_topicProcess->lastSlice && !reachedTotal) { + if (!_topicProcess->lastSlice && !reachedTill && !reachedTotal) { requestTopicMessagesSlice(); } else { finishTopicMessages(); diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index d2e41780a80f88..ebc81698eef6c4 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -204,7 +204,7 @@ class ApiWrap { int splitIndex); void requestMessagesCount(int localSplitIndex); - void checkFirstMessageDate(int localSplitIndex, int count); + void requestDateLimitedMessagesCount(int localSplitIndex); void messagesCountLoaded(int localSplitIndex, int count); void requestMessagesSlice(); void requestChatMessages( @@ -212,13 +212,15 @@ class ApiWrap { int offsetId, int addOffset, int limit, - FnMut done); + FnMut done, + int offsetDate = 0); void requestTopicMessagesSlice(); void requestTopicReplies( int offsetId, int addOffset, int limit, - FnMut done); + FnMut done, + int offsetDate = 0); void collectMessagesCustomEmoji(const Data::MessagesSlice &slice); void resolveCustomEmoji(); void loadMessagesFiles(Data::MessagesSlice &&slice); diff --git a/changelog.txt b/changelog.txt index 678e1d6d820ad9..5937a7ee5496f9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ 6.8.3 beta (19.05.26) +- Export now jumps to the picked "from date" instead of walking the whole chat, and shows progress against the date-filtered total. - In-app markdown file viewer. - Native Instant View support. - Native external webapp viewer on Linux.