Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Telegram/SourceFiles/export/data/export_data_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
199 changes: 159 additions & 40 deletions Telegram/SourceFiles/export/export_api_wrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ struct ApiWrap::TopicProcess : AbstractMessagesProcess {
int32 offsetId = 0;
int totalCount = 0;
int processedCount = 0;
bool firstSliceRequested = false;
};


Expand Down Expand Up @@ -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
Expand All @@ -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<MTPReaction>(), // 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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -1896,15 +1964,17 @@ void ApiWrap::requestMessagesSlice() {
data.vchats(),
_chatProcess->info.relativePath));
});
});
},
offsetDate);
}

void ApiWrap::requestChatMessages(
int splitIndex,
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done) {
FnMut<void(MTPmessages_Messages&&)> done,
int offsetDate) {
Expects(_chatProcess != nullptr);

_chatProcess->requestDone = std::move(done);
Expand All @@ -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),
Expand All @@ -1933,8 +2016,8 @@ void ApiWrap::requestChatMessages(
MTPVector<MTPReaction>(), // 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),
Expand All @@ -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
Expand All @@ -1967,7 +2050,8 @@ void ApiWrap::requestChatMessages(
offsetId,
addOffset,
limit,
base::take(_chatProcess->requestDone));
base::take(_chatProcess->requestDone),
offsetDate);
return true;
}
}
Expand Down Expand Up @@ -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[
Expand All @@ -2206,6 +2296,9 @@ void ApiWrap::finishMessagesSlice() {
return;
}
}
if (reachedTill) {
_chatProcess->lastSlice = true;
}
if (_chatProcess->lastSlice
&& (++_chatProcess->localSplitIndex
< _chatProcess->info.splits.size())) {
Expand Down Expand Up @@ -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,
Expand All @@ -2441,14 +2549,16 @@ void ApiWrap::requestTopicMessagesSlice() {
}
loadTopicMessagesFiles(std::move(slice));
});
});
},
offsetDate);
}

void ApiWrap::requestTopicReplies(
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done) {
FnMut<void(MTPmessages_Messages&&)> done,
int offsetDate) {
Expects(_topicProcess != nullptr);

_topicProcess->requestDone = std::move(done);
Expand All @@ -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),
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
8 changes: 5 additions & 3 deletions Telegram/SourceFiles/export/export_api_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,21 +204,23 @@ 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(
int splitIndex,
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done);
FnMut<void(MTPmessages_Messages&&)> done,
int offsetDate = 0);
void requestTopicMessagesSlice();
void requestTopicReplies(
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done);
FnMut<void(MTPmessages_Messages&&)> done,
int offsetDate = 0);
void collectMessagesCustomEmoji(const Data::MessagesSlice &slice);
void resolveCustomEmoji();
void loadMessagesFiles(Data::MessagesSlice &&slice);
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading