diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9b9636edcf..3bd8414182 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index bde995d42b..f23c279f9d 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -225,25 +225,41 @@ struct ApiWrap::DialogsProcess : ChatsProcess { MTPInputPeer offsetPeer = MTP_inputPeerEmpty(); }; -struct ApiWrap::ChatProcess { - Data::DialogInfo info; - - FnMut start; +struct ApiWrap::AbstractMessagesProcess { Fn fileProgress; Fn handleSlice; FnMut done; FnMut requestDone; - int localSplitIndex = 0; - int32 largestIdPlusOne = 1; - Data::ParseMediaContext context; std::optional slice; bool lastSlice = false; int fileIndex = 0; }; +struct ApiWrap::ChatProcess : AbstractMessagesProcess { + Data::DialogInfo info; + + FnMut start; + + int localSplitIndex = 0; + int32 largestIdPlusOne = 1; +}; + +struct ApiWrap::TopicProcess : AbstractMessagesProcess { + PeerId peerId = 0; + MTPInputPeer inputPeer; + int32 topicRootId = 0; + QString relativePath; + + FnMut start; + + int32 offsetId = 0; + int totalCount = 0; + int processedCount = 0; +}; + template class ApiWrap::RequestBuilder { @@ -2080,13 +2096,18 @@ std::optional 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 start, + Fn progress, + Fn slice, + FnMut done) { + Expects(_topicProcess == nullptr); + Expects(_selfId.has_value()); + + _topicProcess = std::make_unique(); + _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( + 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( + 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()) { + _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 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(); + 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(v) + )).fail([=](const MTP::Error &error) { + LOG(("Export Error: Failed to get documents for emoji.")); + finalize(); + return true; + }).done([=](const MTPVector &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, diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index 4d43cd508d..d2e41780a8 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -106,6 +106,15 @@ public: Fn slice, FnMut done); + void requestTopicMessages( + PeerId peerId, + MTPInputPeer inputPeer, + int32 topicRootId, + FnMut start, + Fn progress, + Fn slice, + FnMut done); + void finishExport(FnMut 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 done); void sendNextStartRequest(); @@ -202,6 +213,12 @@ private: int addOffset, int limit, FnMut done); + void requestTopicMessagesSlice(); + void requestTopicReplies( + int offsetId, + int addOffset, + int limit, + FnMut 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; std::unique_ptr _dialogsProcess; std::unique_ptr _chatProcess; + std::unique_ptr _topicProcess; base::flat_set _unresolvedCustomEmoji; base::flat_map _resolvedCustomEmoji; QVector _splits; diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp index ba4714c485..5c572535ce 100644 --- a/Telegram/SourceFiles/export/export_controller.cpp +++ b/Telegram/SourceFiles/export/export_controller.cpp @@ -37,6 +37,12 @@ public: crl::weak_on_queue weak, QPointer mtproto, const MTPInputPeer &peer); + ControllerObject( + crl::weak_on_queue weak, + QPointer mtproto, + const MTPInputPeer &peer, + int32 topicRootId, + uint64 peerId); rpl::producer state() const; @@ -82,6 +88,7 @@ private: void exportOtherData(); void exportDialogs(); void exportNextDialog(); + void exportTopic(); template 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 _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 weak, + QPointer 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 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(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 mtproto, + const MTPInputPeer &peer, + int32 topicRootId, + uint64 peerId) +: _wrapped( + std::move(mtproto), + peer, + static_cast(topicRootId), + static_cast(peerId)) { +} + rpl::producer Controller::state() const { return _wrapped.producer_on_main([=](const Implementation &unwrapped) { return unwrapped.state(); diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h index 5209f9a6ed..018f48ae77 100644 --- a/Telegram/SourceFiles/export/export_controller.h +++ b/Telegram/SourceFiles/export/export_controller.h @@ -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 mtproto, const MTPInputPeer &peer); + Controller( + QPointer mtproto, + const MTPInputPeer &peer, + int32 topicRootId, + uint64 peerId); rpl::producer state() const; diff --git a/Telegram/SourceFiles/export/export_manager.cpp b/Telegram/SourceFiles/export/export_manager.cpp index 7277adef33..031fe800e3 100644 --- a/Telegram/SourceFiles/export/export_manager.cpp +++ b/Telegram/SourceFiles/export/export_manager.cpp @@ -25,6 +25,21 @@ void Manager::start(not_null peer) { start(&peer->session(), peer->input); } +void Manager::startTopic( + not_null peer, + MsgId topicRootId) { + if (_panel) { + _panel->activatePanel(); + return; + } + _controller = std::make_unique( + &peer->session().mtp(), + peer->input, + int32(topicRootId.bare), + uint64(peer->id.value)); + setupPanel(&peer->session()); +} + void Manager::start( not_null session, const MTPInputPeer &singlePeer) { @@ -35,6 +50,10 @@ void Manager::start( _controller = std::make_unique( &session->mtp(), singlePeer); + setupPanel(session); +} + +void Manager::setupPanel(not_null session) { _panel = std::make_unique( session, _controller.get()); diff --git a/Telegram/SourceFiles/export/export_manager.h b/Telegram/SourceFiles/export/export_manager.h index 0d69a5747f..c9355f352c 100644 --- a/Telegram/SourceFiles/export/export_manager.h +++ b/Telegram/SourceFiles/export/export_manager.h @@ -34,6 +34,9 @@ public: void start( not_null session, const MTPInputPeer &singlePeer = MTP_inputPeerEmpty()); + void startTopic( + not_null peer, + MsgId topicRootId); [[nodiscard]] rpl::producer currentView() const; [[nodiscard]] bool inProgress() const; @@ -42,6 +45,8 @@ public: void stop(); private: + void setupPanel(not_null session); + std::unique_ptr _controller; std::unique_ptr _panel; rpl::event_stream _viewChanges; diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h index b19024e81b..587cab6200 100644 --- a/Telegram/SourceFiles/export/export_settings.h +++ b/Telegram/SourceFiles/export/export_settings.h @@ -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 diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp index fad7f99220..8e0cf0e550 100644 --- a/Telegram/SourceFiles/export/view/export_view_content.cpp +++ b/Telegram/SourceFiles/export/view/export_view_content.cpp @@ -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; diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index d65be8f077..bef37dacc0 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -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::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); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 81822307db..296c10a88d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -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 navigation, + not_null peer, + MsgId topicRootId) { + base::call_delayed(st::defaultPopupMenu.showDuration, [=] { + Core::App().exportManager().startTopic(peer, topicRootId); + }); +} + void PeerMenuDeleteContact( not_null controller, not_null user) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 31d1a9b9fe..5c5718efdb 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -98,6 +98,10 @@ void MenuAddMarkAsReadChatListAction( void PeerMenuExportChat( not_null controller, not_null peer); +void PeerMenuExportTopic( + not_null navigation, + not_null peer, + MsgId topicRootId); void PeerMenuDeleteContact( not_null controller, not_null user);