Added initial ability to export topic history.

This commit is contained in:
23rd
2025-11-25 06:19:34 +03:00
parent 4eee00d95e
commit 57411b962f
12 changed files with 608 additions and 11 deletions

View File

@@ -1735,6 +1735,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_block_user" = "Block user";
"lng_profile_unblock_user" = "Unblock user";
"lng_profile_export_chat" = "Export chat history";
"lng_profile_export_topic" = "Export topic history";
"lng_profile_gift_premium" = "Gift Premium";
"lng_media_selected_photo#one" = "{count} Photo";
"lng_media_selected_photo#other" = "{count} Photos";
@@ -6312,6 +6313,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_export_option_other" = "Miscellaneous data";
"lng_export_option_other_about" = "Other types of data not mentioned above (beta).";
"lng_export_header_chats" = "Chat export settings";
"lng_export_header_topic" = "Topic export settings";
"lng_export_option_personal_chats" = "Personal chats";
"lng_export_option_bot_chats" = "Bot chats";
"lng_export_option_private_groups" = "Private groups";

View File

@@ -225,25 +225,41 @@ struct ApiWrap::DialogsProcess : ChatsProcess {
MTPInputPeer offsetPeer = MTP_inputPeerEmpty();
};
struct ApiWrap::ChatProcess {
Data::DialogInfo info;
FnMut<bool(const Data::DialogInfo &)> start;
struct ApiWrap::AbstractMessagesProcess {
Fn<bool(DownloadProgress)> fileProgress;
Fn<bool(Data::MessagesSlice&&)> handleSlice;
FnMut<void()> done;
FnMut<void(MTPmessages_Messages&&)> requestDone;
int localSplitIndex = 0;
int32 largestIdPlusOne = 1;
Data::ParseMediaContext context;
std::optional<Data::MessagesSlice> slice;
bool lastSlice = false;
int fileIndex = 0;
};
struct ApiWrap::ChatProcess : AbstractMessagesProcess {
Data::DialogInfo info;
FnMut<bool(const Data::DialogInfo &)> start;
int localSplitIndex = 0;
int32 largestIdPlusOne = 1;
};
struct ApiWrap::TopicProcess : AbstractMessagesProcess {
PeerId peerId = 0;
MTPInputPeer inputPeer;
int32 topicRootId = 0;
QString relativePath;
FnMut<bool(int count)> start;
int32 offsetId = 0;
int totalCount = 0;
int processedCount = 0;
};
template <typename Request>
class ApiWrap::RequestBuilder {
@@ -2080,13 +2096,18 @@ std::optional<QByteArray> ApiWrap::getCustomEmoji(QByteArray &data) {
}
auto &file = i->second.file;
const auto fileProgress = [=](FileProgress value) {
return loadMessageEmojiProgress(value);
if (_chatProcess) {
return loadMessageEmojiProgress(value);
} else if (_topicProcess) {
return loadTopicEmojiProgress(value);
}
return true;
};
const auto ready = processFileLoad(
file,
{ .customEmojiId = id },
fileProgress,
[=](const QString &path) { loadMessageEmojiDone(id, path); });
[=](const QString &path) { loadCustomEmojiDone(id, path); });
if (!ready) {
return std::nullopt;
}
@@ -2262,6 +2283,36 @@ void ApiWrap::loadMessageEmojiDone(uint64 id, const QString &relativePath) {
loadNextMessageFile();
}
bool ApiWrap::loadTopicEmojiProgress(FileProgress progress) {
Expects(_fileProcess != nullptr);
Expects(_topicProcess != nullptr);
Expects(_topicProcess->slice.has_value());
Expects((_topicProcess->fileIndex >= 0)
&& (_topicProcess->fileIndex < _topicProcess->slice->list.size()));
return _topicProcess->fileProgress(DownloadProgress{
.randomId = _fileProcess->randomId,
.path = _fileProcess->relativePath,
.itemIndex = _topicProcess->fileIndex,
.ready = progress.ready,
.total = progress.total });
}
void ApiWrap::loadCustomEmojiDone(uint64 id, const QString &relativePath) {
const auto i = _resolvedCustomEmoji.find(id);
if (i != end(_resolvedCustomEmoji)) {
i->second.file.relativePath = relativePath;
if (relativePath.isEmpty()) {
i->second.file.skipReason = Data::File::SkipReason::Unavailable;
}
}
if (_chatProcess) {
loadNextMessageFile();
} else if (_topicProcess) {
loadNextTopicMessageFile();
}
}
void ApiWrap::finishMessages() {
Expects(_chatProcess != nullptr);
Expects(!_chatProcess->slice.has_value());
@@ -2270,6 +2321,316 @@ void ApiWrap::finishMessages() {
process->done();
}
void ApiWrap::requestTopicMessages(
PeerId peerId,
MTPInputPeer inputPeer,
int32 topicRootId,
FnMut<bool(int count)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::MessagesSlice&&)> slice,
FnMut<void()> done) {
Expects(_topicProcess == nullptr);
Expects(_selfId.has_value());
_topicProcess = std::make_unique<TopicProcess>();
_topicProcess->context.selfPeerId = peerFromUser(*_selfId);
_topicProcess->peerId = peerId;
_topicProcess->inputPeer = inputPeer;
_topicProcess->topicRootId = topicRootId;
_topicProcess->relativePath = "chats/chat_"
+ QString::number(peerId.value)
+ "/topic_"
+ QString::number(topicRootId)
+ "/";
_topicProcess->start = std::move(start);
_topicProcess->fileProgress = std::move(progress);
_topicProcess->handleSlice = std::move(slice);
_topicProcess->done = std::move(done);
mainRequest(MTPchannels_GetMessages(
MTP_inputChannel(
inputPeer.c_inputPeerChannel().vchannel_id(),
inputPeer.c_inputPeerChannel().vaccess_hash()),
MTP_vector<MTPInputMessage>(
1,
MTP_inputMessageID(MTP_int(topicRootId)))
)).done([=](const MTPmessages_Messages &rootResult) {
Expects(_topicProcess != nullptr);
auto rootSlice = rootResult.match([&](
const MTPDmessages_messagesNotModified &) {
return Data::MessagesSlice();
}, [&](const auto &data) {
return Data::ParseMessagesSlice(
_topicProcess->context,
data.vmessages(),
data.vusers(),
data.vchats(),
_topicProcess->relativePath);
});
auto rootSlicePtr = std::make_shared<Data::MessagesSlice>(
std::move(rootSlice));
requestTopicReplies(
0,
0,
kMessagesSliceLimit,
[=](const MTPmessages_Messages &result) {
Expects(_topicProcess != nullptr);
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 &data) {
return -1;
});
if (count < 0) {
error("Unexpected messagesNotModified received.");
return;
}
_topicProcess->totalCount = count;
if (!_topicProcess->start(count)) {
return;
}
if (!rootSlicePtr->list.empty()) {
collectMessagesCustomEmoji(*rootSlicePtr);
_topicProcess->slice = std::move(*rootSlicePtr);
_topicProcess->fileIndex = 0;
resolveTopicCustomEmoji();
return;
}
requestTopicMessagesSlice();
});
}).send();
}
void ApiWrap::requestTopicMessagesSlice() {
Expects(_topicProcess != nullptr);
const auto offsetId = (_topicProcess->offsetId == 0)
? 1
: (_topicProcess->offsetId + 1);
requestTopicReplies(
offsetId,
-kMessagesSliceLimit,
kMessagesSliceLimit,
[=](const MTPmessages_Messages &result) {
Expects(_topicProcess != nullptr);
result.match([&](const MTPDmessages_messagesNotModified &data) {
error("Unexpected messagesNotModified received.");
}, [&](const auto &data) {
if constexpr (MTPDmessages_messages::Is<decltype(data)>()) {
_topicProcess->lastSlice = true;
}
auto slice = Data::ParseMessagesSlice(
_topicProcess->context,
data.vmessages(),
data.vusers(),
data.vchats(),
_topicProcess->relativePath);
if (slice.list.empty()) {
_topicProcess->lastSlice = true;
}
loadTopicMessagesFiles(std::move(slice));
});
});
}
void ApiWrap::requestTopicReplies(
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done) {
Expects(_topicProcess != nullptr);
_topicProcess->requestDone = std::move(done);
const auto doneHandler = [=](MTPmessages_Messages &&result) {
Expects(_topicProcess != nullptr);
base::take(_topicProcess->requestDone)(std::move(result));
};
mainRequest(MTPmessages_GetReplies(
_topicProcess->inputPeer,
MTP_int(_topicProcess->topicRootId),
MTP_int(offsetId),
MTP_int(0),
MTP_int(addOffset),
MTP_int(limit),
MTP_int(0),
MTP_int(0),
MTP_long(0)
)).done(doneHandler).send();
}
void ApiWrap::loadTopicMessagesFiles(Data::MessagesSlice &&slice) {
Expects(_topicProcess != nullptr);
Expects(!_topicProcess->slice.has_value());
collectMessagesCustomEmoji(slice);
if (slice.list.empty()) {
_topicProcess->lastSlice = true;
}
_topicProcess->slice = std::move(slice);
_topicProcess->fileIndex = 0;
resolveTopicCustomEmoji();
}
void ApiWrap::resolveTopicCustomEmoji() {
if (_unresolvedCustomEmoji.empty()) {
loadNextTopicMessageFile();
return;
}
const auto count = std::min(
int(_unresolvedCustomEmoji.size()),
kMaxEmojiPerRequest);
auto v = QVector<MTPlong>();
v.reserve(count);
const auto till = end(_unresolvedCustomEmoji);
const auto from = end(_unresolvedCustomEmoji) - count;
for (auto i = from; i != till; ++i) {
v.push_back(MTP_long(*i));
}
_unresolvedCustomEmoji.erase(from, till);
const auto finalize = [=] {
for (const auto &id : v) {
if (_resolvedCustomEmoji.contains(id.v)) {
continue;
}
_resolvedCustomEmoji.emplace(
id.v,
Data::Document{
.file = {
.skipReason = Data::File::SkipReason::Unavailable,
},
});
}
resolveTopicCustomEmoji();
};
mainRequest(MTPmessages_GetCustomEmojiDocuments(
MTP_vector<MTPlong>(v)
)).fail([=](const MTP::Error &error) {
LOG(("Export Error: Failed to get documents for emoji."));
finalize();
return true;
}).done([=](const MTPVector<MTPDocument> &result) {
for (const auto &entry : result.v) {
auto document = Data::ParseDocument(
_topicProcess->context,
entry,
_topicProcess->relativePath,
TimeId());
_resolvedCustomEmoji.emplace(document.id, std::move(document));
}
finalize();
}).send();
}
void ApiWrap::loadNextTopicMessageFile() {
Expects(_topicProcess != nullptr);
Expects(_topicProcess->slice.has_value());
const auto makeProgress = [=](FileProgress progress) {
return _topicProcess->fileProgress(DownloadProgress{
.randomId = _fileProcess->randomId,
.path = _fileProcess->relativePath,
.itemIndex = _topicProcess->fileIndex,
.ready = progress.ready,
.total = progress.total,
});
};
for (auto &list = _topicProcess->slice->list
; _topicProcess->fileIndex < list.size()
; ++_topicProcess->fileIndex) {
auto &message = list[_topicProcess->fileIndex];
if (!messageCustomEmojiReady(message)) {
return;
}
const auto origin = Data::FileOrigin{
.peer = _topicProcess->inputPeer,
.messageId = message.id
};
const auto ready = processFileLoad(
message.file(),
origin,
makeProgress,
[=, &message](const QString &path) {
loadTopicMessageFileOrThumbDone(message.file(), path);
},
&message);
if (!ready) {
return;
}
const auto thumbReady = processFileLoad(
message.thumb().file,
origin,
makeProgress,
[=, &message](const QString &path) {
loadTopicMessageFileOrThumbDone(message.thumb().file, path);
},
&message);
if (!thumbReady) {
return;
}
}
finishTopicMessagesSlice();
}
void ApiWrap::finishTopicMessagesSlice() {
Expects(_topicProcess != nullptr);
Expects(_topicProcess->slice.has_value());
auto slice = *base::take(_topicProcess->slice);
if (!slice.list.empty()) {
_topicProcess->offsetId = slice.list.back().id;
_topicProcess->processedCount += slice.list.size();
if (!_topicProcess->handleSlice(std::move(slice))) {
return;
}
}
const auto reachedTotal = _topicProcess->totalCount > 0
&& _topicProcess->processedCount >= _topicProcess->totalCount;
if (!_topicProcess->lastSlice && !reachedTotal) {
requestTopicMessagesSlice();
} else {
finishTopicMessages();
}
}
void ApiWrap::loadTopicMessageFileOrThumbDone(
Data::File &file,
const QString &relativePath) {
Expects(_topicProcess != nullptr);
Expects(_topicProcess->slice.has_value());
Expects((_topicProcess->fileIndex >= 0)
&& (_topicProcess->fileIndex < _topicProcess->slice->list.size()));
file.relativePath = relativePath;
if (relativePath.isEmpty()) {
file.skipReason = Data::File::SkipReason::Unavailable;
}
loadNextTopicMessageFile();
}
void ApiWrap::finishTopicMessages() {
Expects(_topicProcess != nullptr);
Expects(!_topicProcess->slice.has_value());
const auto process = base::take(_topicProcess);
process->done();
}
bool ApiWrap::processFileLoad(
Data::File &file,
const Data::FileOrigin &origin,

View File

@@ -106,6 +106,15 @@ public:
Fn<bool(Data::MessagesSlice&&)> slice,
FnMut<void()> done);
void requestTopicMessages(
PeerId peerId,
MTPInputPeer inputPeer,
int32 topicRootId,
FnMut<bool(int count)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::MessagesSlice&&)> slice,
FnMut<void()> done);
void finishExport(FnMut<void()> done);
void skipFile(uint64 randomId);
void cancelExportFast();
@@ -125,7 +134,9 @@ private:
struct ChatsProcess;
struct LeftChannelsProcess;
struct DialogsProcess;
struct AbstractMessagesProcess;
struct ChatProcess;
struct TopicProcess;
void startMainSession(FnMut<void()> done);
void sendNextStartRequest();
@@ -202,6 +213,12 @@ private:
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done);
void requestTopicMessagesSlice();
void requestTopicReplies(
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done);
void collectMessagesCustomEmoji(const Data::MessagesSlice &slice);
void resolveCustomEmoji();
void loadMessagesFiles(Data::MessagesSlice &&slice);
@@ -217,6 +234,17 @@ private:
void finishMessagesSlice();
void finishMessages();
void loadTopicMessagesFiles(Data::MessagesSlice &&slice);
void resolveTopicCustomEmoji();
void loadNextTopicMessageFile();
bool loadTopicEmojiProgress(FileProgress progress);
void loadCustomEmojiDone(uint64 id, const QString &relativePath);
void loadTopicMessageFileOrThumbDone(
Data::File &file,
const QString &relativePath);
void finishTopicMessagesSlice();
void finishTopicMessages();
[[nodiscard]] Data::Message *currentFileMessage() const;
[[nodiscard]] Data::FileOrigin currentFileMessageOrigin() const;
@@ -285,6 +313,7 @@ private:
std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
std::unique_ptr<DialogsProcess> _dialogsProcess;
std::unique_ptr<ChatProcess> _chatProcess;
std::unique_ptr<TopicProcess> _topicProcess;
base::flat_set<uint64> _unresolvedCustomEmoji;
base::flat_map<uint64, Data::Document> _resolvedCustomEmoji;
QVector<MTPMessageRange> _splits;

View File

@@ -37,6 +37,12 @@ public:
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer);
ControllerObject(
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId);
rpl::producer<State> state() const;
@@ -82,6 +88,7 @@ private:
void exportOtherData();
void exportDialogs();
void exportNextDialog();
void exportTopic();
template <typename Callback = const decltype(kNullStateCallback) &>
ProcessingState prepareState(
@@ -97,6 +104,7 @@ private:
ProcessingState stateSessions() const;
ProcessingState stateOtherData() const;
ProcessingState stateDialogs(const DownloadProgress &progress) const;
ProcessingState stateTopic(const DownloadProgress &progress) const;
void fillMessagesState(
ProcessingState &result,
const Data::DialogsInfo &info,
@@ -139,6 +147,9 @@ private:
std::vector<Step> _steps;
int _stepIndex = -1;
int32 _topicRootId = 0;
uint64 _topicPeerId = 0;
rpl::lifetime _lifetime;
};
@@ -167,6 +178,34 @@ ControllerObject::ControllerObject(
setState(std::move(state));
}
ControllerObject::ControllerObject(
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId)
: _api(mtproto, weak.runner())
, _state(PasswordCheckState{})
, _topicRootId(topicRootId)
, _topicPeerId(peerId) {
_api.errors(
) | rpl::start_with_next([=](const MTP::Error &error) {
setState(ApiErrorState{ error });
}, _lifetime);
_api.ioErrors(
) | rpl::start_with_next([=](const Output::Result &result) {
ioCatchError(result);
}, _lifetime);
//requestPasswordState();
auto state = PasswordCheckState();
state.checked = false;
state.requesting = false;
state.singlePeer = peer;
setState(std::move(state));
}
rpl::producer<State> ControllerObject::state() const {
return rpl::single(
_state
@@ -257,6 +296,8 @@ void ControllerObject::startExport(
}
_settings = NormalizeSettings(settings);
_environment = environment;
_settings.singleTopicRootId = _topicRootId;
_settings.singleTopicPeerId = _topicPeerId;
_settings.path = Output::NormalizePath(_settings);
_writer = Output::CreateWriter(_settings.format);
@@ -274,6 +315,10 @@ void ControllerObject::skipFile(uint64 randomId) {
void ControllerObject::fillExportSteps() {
using Type = Settings::Type;
_steps.push_back(Step::Initializing);
if (_settings.onlySingleTopic()) {
_steps.push_back(Step::Topic);
return;
}
if (_settings.types & Type::AnyChatsMask) {
_steps.push_back(Step::DialogsList);
}
@@ -340,6 +385,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
if (_settings.types & Settings::Type::AnyChatsMask) {
push(Step::Dialogs, info.dialogsCount);
}
if (_settings.onlySingleTopic()) {
push(Step::Topic, 1);
}
_substepsInStep = std::move(result);
_substepsTotal = ranges::accumulate(_substepsInStep, 0);
}
@@ -372,6 +420,7 @@ void ControllerObject::exportNext() {
case Step::Sessions: return exportSessions();
case Step::OtherData: return exportOtherData();
case Step::Dialogs: return exportDialogs();
case Step::Topic: return exportTopic();
}
Unexpected("Step in ControllerObject::exportNext.");
}
@@ -708,6 +757,70 @@ int ControllerObject::substepsInStep(Step step) const {
return _substepsInStep[static_cast<int>(step)];
}
void ControllerObject::exportTopic() {
auto topicInfo = Data::DialogInfo();
topicInfo.type = Data::DialogInfo::Type::PublicSupergroup;
topicInfo.name = "Topic";
topicInfo.peerId = PeerId(_topicPeerId);
topicInfo.relativePath = QString();
if (ioCatchError(_writer->writeDialogStart(topicInfo))) {
return;
}
_api.requestTopicMessages(
PeerId(_topicPeerId),
_settings.singlePeer,
_topicRootId,
[=](int count) {
_messagesWritten = 0;
_messagesCount = count;
setState(stateTopic(DownloadProgress()));
return true;
},
[=](DownloadProgress progress) {
setState(stateTopic(progress));
return true;
},
[=](Data::MessagesSlice &&slice) {
if (ioCatchError(_writer->writeDialogSlice(slice))) {
return false;
}
_messagesWritten += slice.list.size();
setState(stateTopic(DownloadProgress()));
return true;
},
[=] {
if (ioCatchError(_writer->writeDialogEnd())) {
return;
}
if (ioCatchError(_writer->finish())) {
return;
}
_api.finishExport([=] {
setFinishedState();
});
});
}
ProcessingState ControllerObject::stateTopic(
const DownloadProgress &progress) const {
return prepareState(Step::Topic, [&](ProcessingState &result) {
result.entityType = ProcessingState::EntityType::Topic;
result.entityIndex = 0;
result.entityCount = 1;
result.itemIndex = _messagesWritten + progress.itemIndex;
result.itemCount = std::max(_messagesCount, result.itemIndex);
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
}
result.bytesLoaded = progress.ready;
result.bytesCount = progress.total;
});
}
void ControllerObject::setFinishedState() {
setState(FinishedState{
_writer->mainFilePath(),
@@ -721,6 +834,18 @@ Controller::Controller(
: _wrapped(std::move(mtproto), peer) {
}
Controller::Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId)
: _wrapped(
std::move(mtproto),
peer,
static_cast<int32>(topicRootId),
static_cast<uint64>(peerId)) {
}
rpl::producer<State> Controller::state() const {
return _wrapped.producer_on_main([=](const Implementation &unwrapped) {
return unwrapped.state();

View File

@@ -44,12 +44,14 @@ struct ProcessingState {
Sessions,
OtherData,
Dialogs,
Topic,
};
enum class EntityType {
Chat,
SavedMessages,
RepliesMessages,
VerifyCodes,
Topic,
Other,
};
@@ -115,6 +117,11 @@ public:
Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer);
Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId);
rpl::producer<State> state() const;

View File

@@ -25,6 +25,21 @@ void Manager::start(not_null<PeerData*> peer) {
start(&peer->session(), peer->input);
}
void Manager::startTopic(
not_null<PeerData*> peer,
MsgId topicRootId) {
if (_panel) {
_panel->activatePanel();
return;
}
_controller = std::make_unique<Controller>(
&peer->session().mtp(),
peer->input,
int32(topicRootId.bare),
uint64(peer->id.value));
setupPanel(&peer->session());
}
void Manager::start(
not_null<Main::Session*> session,
const MTPInputPeer &singlePeer) {
@@ -35,6 +50,10 @@ void Manager::start(
_controller = std::make_unique<Controller>(
&session->mtp(),
singlePeer);
setupPanel(session);
}
void Manager::setupPanel(not_null<Main::Session*> session) {
_panel = std::make_unique<View::PanelController>(
session,
_controller.get());

View File

@@ -34,6 +34,9 @@ public:
void start(
not_null<Main::Session*> session,
const MTPInputPeer &singlePeer = MTP_inputPeerEmpty());
void startTopic(
not_null<PeerData*> peer,
MsgId topicRootId);
[[nodiscard]] rpl::producer<View::PanelController*> currentView() const;
[[nodiscard]] bool inProgress() const;
@@ -42,6 +45,8 @@ public:
void stop();
private:
void setupPanel(not_null<Main::Session*> session);
std::unique_ptr<Controller> _controller;
std::unique_ptr<View::PanelController> _panel;
rpl::event_stream<View::PanelController*> _viewChanges;

View File

@@ -88,12 +88,19 @@ struct Settings {
TimeId singlePeerFrom = 0;
TimeId singlePeerTill = 0;
int32 singleTopicRootId = 0;
uint64 singleTopicPeerId = 0;
TimeId availableAt = 0;
bool onlySinglePeer() const {
return singlePeer.type() != mtpc_inputPeerEmpty;
}
bool onlySingleTopic() const {
return onlySinglePeer() && singleTopicRootId != 0;
}
static inline Types DefaultTypes() {
return Type::PersonalInfo
| Type::Userpics

View File

@@ -138,6 +138,24 @@ Content ContentFromState(
state.bytesName,
state.bytesRandomId);
break;
case Step::Topic:
pushMain(tr::lng_export_state_chats(tr::now));
push(
"topic",
u"Topic"_q,
(state.itemCount > 0
? (QString::number(state.itemIndex)
+ " / "
+ QString::number(state.itemCount))
: QString()),
(state.itemCount > 0
? (state.itemIndex / float64(state.itemCount))
: 0.));
pushBytes(
"file_topic_" + QString::number(state.itemIndex),
state.bytesName,
state.bytesRandomId);
break;
default: Unexpected("Step in ContentFromState.");
}
const auto requiredRows = settings->onlySinglePeer() ? 2 : 3;

View File

@@ -168,10 +168,13 @@ void PanelController::activatePanel() {
void PanelController::createPanel() {
const auto singlePeer = _settings->onlySinglePeer();
const auto singleTopic = _settings->onlySingleTopic();
_panel = base::make_unique_q<Ui::SeparatePanel>(Ui::SeparatePanelArgs{
.onAllSpaces = true,
});
_panel->setTitle((singlePeer
_panel->setTitle((singleTopic
? tr::lng_export_header_topic
: singlePeer
? tr::lng_export_header_chats
: tr::lng_export_title)());
_panel->setInnerSize(st::exportPanelSize);

View File

@@ -906,11 +906,19 @@ void Filler::addDirectMessages() {
}
void Filler::addExportChat() {
if (_thread->asTopic() || !_peer->canExportChatHistory()) {
if (!_peer->canExportChatHistory()) {
return;
}
const auto peer = _peer;
const auto navigation = _controller;
if (const auto topic = _thread->asTopic()) {
const auto topicRootId = topic->rootId();
_addAction(
tr::lng_profile_export_topic(tr::now),
[=] { PeerMenuExportTopic(navigation, peer, topicRootId); },
&st::menuIconExport);
return;
}
_addAction(
tr::lng_profile_export_chat(tr::now),
[=] { PeerMenuExportChat(navigation, peer); },
@@ -1746,6 +1754,15 @@ void PeerMenuExportChat(
});
}
void PeerMenuExportTopic(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId) {
base::call_delayed(st::defaultPopupMenu.showDuration, [=] {
Core::App().exportManager().startTopic(peer, topicRootId);
});
}
void PeerMenuDeleteContact(
not_null<Window::SessionController*> controller,
not_null<UserData*> user) {

View File

@@ -98,6 +98,10 @@ void MenuAddMarkAsReadChatListAction(
void PeerMenuExportChat(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer);
void PeerMenuExportTopic(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId);
void PeerMenuDeleteContact(
not_null<Window::SessionController*> controller,
not_null<UserData*> user);