From 16a23f78fadea100afc32bfd8c9d939df80dfd41 Mon Sep 17 00:00:00 2001 From: Lertov2424232 <274903558+Lertov2424232@users.noreply.github.com> Date: Mon, 25 May 2026 09:09:13 +0000 Subject: [PATCH 1/3] Export: jump to "from date" instead of walking entire chat history. When the user picks a "From date" for chat / topic export, the exporter still walked messages.getHistory from `offset_id = 1` (i.e. from the very first message of the chat) and filtered each message client-side via `SkipMessageByDate`. On a chat with millions of messages that meant tens of thousands of redundant API calls before reaching the requested range. Use MTP's `offset_date` parameter on the first slice of each split / topic to position the server directly at `singlePeerFrom`, then continue the normal forward walk by `offset_id` once we have a foothold. Additional changes: * `messages.Search` (onlyMyMessages branch) now passes `min_date = singlePeerFrom` and `max_date = singlePeerTill`, which were previously hardcoded to 0. * `finishMessagesSlice` / `finishTopicMessagesSlice` early-stop the loop once the newest message in the current batch is at or past `singlePeerTill`, instead of walking to the end of the chat just to client-side-skip the tail. * `TopicProcess` gains a `firstSliceRequested` flag so the topic loop can cleanly distinguish the first slice request from continuation (the existing `offsetId == 0` check could not, since `offsetId` is seeded with `topicRootId`). Behaviour with no date filter (`singlePeerFrom == 0 && singlePeerTill == 0`) is unchanged byte-for-byte; the new branches only activate when the user explicitly picks a date range in the export dialog. --- .../SourceFiles/export/export_api_wrap.cpp | 85 ++++++++++++++++--- Telegram/SourceFiles/export/export_api_wrap.h | 6 +- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index f23c279f9d4236..5a48c6013df63a 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; }; @@ -1875,9 +1876,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 +1910,8 @@ void ApiWrap::requestMessagesSlice() { data.vchats(), _chatProcess->info.relativePath)); }); - }); + }, + offsetDate); } void ApiWrap::requestChatMessages( @@ -1904,7 +1919,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 +1939,14 @@ 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. + const auto minDate = (_settings->singlePeerFrom > 0) + ? _settings->singlePeerFrom + : 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 +1957,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 +1970,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 +1991,8 @@ void ApiWrap::requestChatMessages( offsetId, addOffset, limit, - base::take(_chatProcess->requestDone)); + base::take(_chatProcess->requestDone), + offsetDate); return true; } } @@ -2195,6 +2220,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 +2237,9 @@ void ApiWrap::finishMessagesSlice() { return; } } + if (reachedTill) { + _chatProcess->lastSlice = true; + } if (_chatProcess->lastSlice && (++_chatProcess->localSplitIndex < _chatProcess->info.splits.size())) { @@ -2414,9 +2448,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 +2490,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 +2512,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), @@ -2590,6 +2641,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 +2658,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..3dcde124f42f7e 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -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); From 29db4416a1710abebc3fd41d82f27f5120b3ca6a Mon Sep 17 00:00:00 2001 From: Lertov2424232 Date: Mon, 25 May 2026 14:08:14 +0000 Subject: [PATCH 2/3] Export: fix from-date boundary and respect range in topic media loader Address edge cases on top of the offset_date jump: * SingleMessageAfter: switch the comparison to >= so a split whose newest message has date == singlePeerFrom is not pre-skipped by the count probe. SkipMessageByDate keeps messages with singlePeerFrom <= date, so the pre-iteration filter must match that inclusive lower bound. * requestChatMessages (onlyMyMessages branch): messages.search uses strict > for min_date and strict < for max_date, while the exporter's canonical range is [singlePeerFrom, singlePeerTill). Pass min_date = singlePeerFrom - 1 to keep messages dated exactly singlePeerFrom; max_date stays singlePeerTill. * loadNextTopicMessageFile: skip media download for messages outside the date range, mirroring loadNextMessageFile for the chat path. Without this, topic exports with a date filter still trigger downloads for out-of-range messages. * changelog.txt: add a user-facing entry. --- Telegram/SourceFiles/export/data/export_data_types.cpp | 5 ++++- Telegram/SourceFiles/export/export_api_wrap.cpp | 10 +++++++++- changelog.txt | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) 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 5a48c6013df63a..2ba7ee9080f692 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -1941,8 +1941,13 @@ void ApiWrap::requestChatMessages( : (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 + ? (_settings->singlePeerFrom - 1) : 0; const auto maxDate = (_settings->singlePeerTill > 0) ? _settings->singlePeerTill @@ -2603,6 +2608,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; } diff --git a/changelog.txt b/changelog.txt index 678e1d6d820ad9..071f6012f221cd 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. - In-app markdown file viewer. - Native Instant View support. - Native external webapp viewer on Linux. From d271597f0fc42d41d4a4f32b8a4313aa7027dea8 Mon Sep 17 00:00:00 2001 From: Lertov2424232 Date: Tue, 26 May 2026 10:09:54 +0000 Subject: [PATCH 3/3] Export: report progress against the date-filtered total. When the user picks a date range, ask the server for the exact count of messages matching the range via messages.search instead of using the full split count. This makes the export progress bar reflect real progress for date-limited exports (previously it stored the whole-chat count and jumped from ~0% to 100% near the end). The same per-split search call also lets us drop the SingleMessageAfter / SingleMessageBefore probes - if the matching count is zero, the split is empty for the picked range and is skipped. Non-date-limited exports are unchanged: requestMessagesCount keeps using messages.getHistory with limit=1 to read the split count. --- .../SourceFiles/export/export_api_wrap.cpp | 106 +++++++++++++----- Telegram/SourceFiles/export/export_api_wrap.h | 2 +- changelog.txt | 2 +- 3 files changed, 82 insertions(+), 28 deletions(-) diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 2ba7ee9080f692..ecb931919bdfea 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -1465,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 @@ -1487,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) { diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index 3dcde124f42f7e..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( diff --git a/changelog.txt b/changelog.txt index 071f6012f221cd..5937a7ee5496f9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ 6.8.3 beta (19.05.26) -- Export now jumps to the picked "from date" instead of walking the whole chat. +- 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.