diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 99a7a20c1..86e39de2b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -672,6 +672,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_delete_conversation" = "Delete conversation"; "lng_profile_block_user" = "Block user"; "lng_profile_unblock_user" = "Unblock user"; +"lng_profile_export_chat" = "Export chat history"; +"lng_profile_export_channel" = "Export channel history"; "lng_media_selected_photo#one" = "{count} Photo"; "lng_media_selected_photo#other" = "{count} Photos"; "lng_media_selected_video#one" = "{count} Video"; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 8ba9d3e46..0aa5df079 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -72,12 +72,16 @@ Session::Session(not_null session) setupChannelLeavingViewer(); } -void Session::startExport() { +void Session::startExport(PeerData *peer) { + startExport(peer ? peer->input : MTP_inputPeerEmpty()); +} + +void Session::startExport(const MTPInputPeer &singlePeer) { if (_exportPanel) { _exportPanel->activatePanel(); return; } - _export = std::make_unique(); + _export = std::make_unique(singlePeer); _exportPanel = std::make_unique( _export.get()); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index e52eef4c8..98d57450a 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -56,7 +56,8 @@ public: return *_session; } - void startExport(); + void startExport(PeerData *peer = nullptr); + void startExport(const MTPInputPeer &singlePeer); void suggestStartExport(TimeId availableAt); void clearExportSuggestion(); rpl::producer currentExportView() const; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index f835dc2f9..5dcc89b60 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1386,19 +1386,36 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) { return result; } +DialogInfo DialogInfoFromUser(const User &data) { + auto result = DialogInfo(); + result.input = (Peer{ data }).input(); + result.name = data.info.firstName; + result.lastName = data.info.lastName; + result.peerId = UserPeerId(data.info.userId); + result.topMessageDate = 0; + result.topMessageId = 0; + result.type = DialogTypeFromUser(data); + result.isLeftChannel = false; + return result; +} + +DialogInfo DialogInfoFromChat(const Chat &data) { + auto result = DialogInfo(); + result.input = data.input; + result.name = data.title; + result.peerId = ChatPeerId(data.id); + result.topMessageDate = 0; + result.topMessageId = 0; + result.type = DialogTypeFromChat(data); + return result; +} + DialogsInfo ParseLeftChannelsInfo(const MTPmessages_Chats &data) { auto result = DialogsInfo(); data.match([&](const auto &data) { //MTPDmessages_chats &data) { result.left.reserve(data.vchats.v.size()); for (const auto &single : data.vchats.v) { - const auto chat = ParseChat(single); - auto info = DialogInfo(); - info.input = chat.input; - info.name = chat.title; - info.peerId = ChatPeerId(chat.id); - info.topMessageDate = 0; - info.topMessageId = 0; - info.type = DialogTypeFromChat(chat); + auto info = DialogInfoFromChat(ParseChat(single)); info.isLeftChannel = true; result.left.push_back(std::move(info)); } @@ -1406,6 +1423,65 @@ DialogsInfo ParseLeftChannelsInfo(const MTPmessages_Chats &data) { return result; } +DialogsInfo ParseDialogsInfo( + const MTPInputPeer &singlePeer, + const MTPVector &data) { + const auto singleId = singlePeer.match( + [](const MTPDinputPeerUser &data) { + return data.vuser_id.v; + }, [](const MTPDinputPeerSelf &data) { + return 0; + }, [](const auto &data) -> int { + Unexpected("Single peer type in ParseDialogsInfo(users)."); + }); + auto result = DialogsInfo(); + result.chats.reserve(data.v.size()); + for (const auto &single : data.v) { + const auto userId = single.match([&](const auto &data) { + return data.vid.v; + }); + if (userId != singleId + && (singleId != 0 + || single.type() != mtpc_user + || !single.c_user().is_self())) { + continue; + } + auto info = DialogInfoFromUser(ParseUser(single)); + result.chats.push_back(std::move(info)); + } + return result; +} + +DialogsInfo ParseDialogsInfo( + const MTPInputPeer &singlePeer, + const MTPmessages_Chats &data) { + const auto singleId = singlePeer.match( + [](const MTPDinputPeerChat &data) { + return data.vchat_id.v; + }, [](const MTPDinputPeerChannel &data) { + return data.vchannel_id.v; + }, [](const auto &data) -> int { + Unexpected("Single peer type in ParseDialogsInfo(chats)."); + }); + auto result = DialogsInfo(); + data.match([&](const auto &data) { //MTPDmessages_chats &data) { + result.chats.reserve(data.vchats.v.size()); + for (const auto &single : data.vchats.v) { + const auto chatId = single.match([&](const auto &data) { + return data.vid.v; + }); + if (chatId != singleId) { + continue; + } + const auto chat = ParseChat(single); + auto info = DialogInfoFromChat(ParseChat(single)); + info.isLeftChannel = false; + result.chats.push_back(std::move(info)); + } + }); + return result; +} + void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) { auto &chats = info.chats; auto &left = info.left; @@ -1414,7 +1490,9 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) { auto index = 0; for (auto &dialog : chats) { const auto number = Data::NumberToString(++index, digits, '0'); - dialog.relativePath = "chats/chat_" + number + '/'; + dialog.relativePath = settings.onlySinglePeer() + ? QString() + : "chats/chat_" + QString::fromUtf8(number) + '/'; using DialogType = DialogInfo::Type; using Type = Settings::Type; @@ -1436,6 +1514,8 @@ void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings) { ranges::reverse(dialog.splits); } for (auto &dialog : left) { + Assert(!settings.onlySinglePeer()); + const auto number = Data::NumberToString(++index, digits, '0'); dialog.relativePath = "chats/chat_" + number + '/'; dialog.onlyMyMessages = true; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index cea1c7dc6..c9de8f3d6 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -546,6 +546,12 @@ struct DialogsInfo { DialogInfo::Type DialogTypeFromChat(const Chat &chat); DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data); +DialogsInfo ParseDialogsInfo( + const MTPInputPeer &singlePeer, + const MTPVector &data); +DialogsInfo ParseDialogsInfo( + const MTPInputPeer &singlePeer, + const MTPmessages_Chats &data); DialogsInfo ParseLeftChannelsInfo(const MTPmessages_Chats &data); void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings); diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index b618937f6..aba06f381 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -369,7 +369,8 @@ auto ApiWrap::fileRequest(const Data::FileLocation &location, int offset) { filePartDone(0, MTP_upload_file(MTP_storage_filePartial(), MTP_int(0), MTP_bytes(QByteArray()))); - } else if (result.type() == qstr("LOCATION_INVALID")) { + } else if (result.type() == qstr("LOCATION_INVALID") + || result.type() == qstr("VERSION_INVALID")) { filePartUnavailable(); } else { error(std::move(result)); @@ -413,7 +414,9 @@ void ApiWrap::startExport( _startProcess->steps.push_back(Step::DialogsCount); } if (_settings->types & Settings::Type::GroupsChannelsMask) { - _startProcess->steps.push_back(Step::LeftChannelsCount); + if (!_settings->onlySinglePeer()) { + _startProcess->steps.push_back(Step::LeftChannelsCount); + } } startMainSession([=] { sendNextStartRequest(); @@ -489,6 +492,15 @@ void ApiWrap::requestSplitRanges() { void ApiWrap::requestDialogsCount() { Expects(_startProcess != nullptr); + if (_settings->onlySinglePeer()) { + _startProcess->info.dialogsCount = + (_settings->singlePeer.type() == mtpc_inputPeerChannel + ? 1 + : _splits.size()); + sendNextStartRequest(); + return; + } + const auto offsetDate = 0; const auto offsetId = 0; const auto offsetPeer = MTP_inputPeerEmpty(); @@ -959,9 +971,58 @@ void ApiWrap::cancelExportFast() { } } +void ApiWrap::requestSinglePeerDialog() { + auto doneSinglePeer = [=](const auto &result) { + auto info = Data::ParseDialogsInfo(_settings->singlePeer, result); + + _dialogsProcess->processedCount += info.chats.size(); + appendDialogsSlice(std::move(info)); + + const auto last = _dialogsProcess->splitIndexPlusOne - 1; + for (auto &info : _dialogsProcess->info.chats) { + for (auto i = last; i != 0; --i) { + info.splits.push_back(i - 1); + info.messagesCountPerSplit.push_back(0); + } + } + + if (!_dialogsProcess->progress(_dialogsProcess->processedCount)) { + return; + } + finishDialogsList(); + }; + const auto requestUser = [&](const MTPInputUser &data) { + mainRequest(MTPusers_GetUsers( + MTP_vector(1, data) + )).done(std::move(doneSinglePeer)).send(); + }; + _settings->singlePeer.match([&](const MTPDinputPeerUser &data) { + requestUser(MTP_inputUser(data.vuser_id, data.vaccess_hash)); + }, [&](const MTPDinputPeerChat &data) { + mainRequest(MTPmessages_GetChats( + MTP_vector(1, data.vchat_id) + )).done(std::move(doneSinglePeer)).send(); + }, [&](const MTPDinputPeerChannel &data) { + mainRequest(MTPchannels_GetChannels( + MTP_vector( + 1, + MTP_inputChannel(data.vchannel_id, data.vaccess_hash)) + )).done(std::move(doneSinglePeer)).send(); + }, [&](const MTPDinputPeerSelf &data) { + requestUser(MTP_inputUserSelf()); + }, [](const MTPDinputPeerEmpty &data) { + Unexpected("Empty peer in ApiWrap::requestSinglePeerDialog."); + }); +} + void ApiWrap::requestDialogsSlice() { Expects(_dialogsProcess != nullptr); + if (_settings->onlySinglePeer()) { + requestSinglePeerDialog(); + return; + } + const auto splitIndex = _dialogsProcess->splitIndexPlusOne - 1; const auto hash = 0; splitRequest(splitIndex, MTPmessages_GetDialogs( diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index e9594c68d..7e3d64e19 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -124,6 +124,7 @@ private: void requestDialogsSlice(); void appendDialogsSlice(Data::DialogsInfo &&info); void finishDialogsList(); + void requestSinglePeerDialog(); void requestLeftChannelsIfNeeded(); void requestLeftChannelsList( diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp index d2467f002..c832879f6 100644 --- a/Telegram/SourceFiles/export/export_controller.cpp +++ b/Telegram/SourceFiles/export/export_controller.cpp @@ -15,12 +15,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "export/output/export_output_stats.h" namespace Export { +namespace { -auto kNullStateCallback = [](ProcessingState&) {}; +const auto kNullStateCallback = [](ProcessingState&) {}; + +Settings NormalizeSettings(const Settings &settings) { + if (!settings.onlySinglePeer()) { + return base::duplicate(settings); + } + auto result = base::duplicate(settings); + result.format = Output::Format::Html; + result.types = result.fullChats = Settings::Type::AnyChatsMask; + return result; +} + +} // namespace class Controller { public: - Controller(crl::weak_on_queue weak); + Controller( + crl::weak_on_queue weak, + const MTPInputPeer &peer); rpl::producer state() const; @@ -83,8 +98,6 @@ private: int substepsInStep(Step step) const; - bool normalizePath(); - ApiWrap _api; Settings _settings; Environment _environment; @@ -117,7 +130,9 @@ private: }; -Controller::Controller(crl::weak_on_queue weak) +Controller::Controller( + crl::weak_on_queue weak, + const MTPInputPeer &peer) : _api(weak.runner()) , _state(PasswordCheckState{}) { _api.errors( @@ -134,6 +149,7 @@ Controller::Controller(crl::weak_on_queue weak) auto state = PasswordCheckState(); state.checked = false; state.requesting = false; + state.singlePeer = peer; setState(std::move(state)); } @@ -218,10 +234,10 @@ void Controller::startExport( if (!_settings.path.isEmpty()) { return; } - _settings = base::duplicate(settings); + _settings = NormalizeSettings(settings); _environment = environment; - _settings.path = Output::NormalizePath(_settings.path); + _settings.path = Output::NormalizePath(_settings); _writer = Output::CreateWriter(_settings.format); fillExportSteps(); exportNext(); @@ -569,7 +585,7 @@ void Controller::setFinishedState() { _stats.bytesCount() }); } -ControllerWrap::ControllerWrap() { +ControllerWrap::ControllerWrap(const MTPInputPeer &peer) : _wrapped(peer) { } rpl::producer ControllerWrap::state() const { diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h index 29c96b26c..26ca4909d 100644 --- a/Telegram/SourceFiles/export/export_controller.h +++ b/Telegram/SourceFiles/export/export_controller.h @@ -23,6 +23,7 @@ struct PasswordCheckState { bool requesting = true; bool hasPassword = false; bool checked = false; + MTPInputPeer singlePeer = MTP_inputPeerEmpty(); }; struct ProcessingState { @@ -110,7 +111,7 @@ using State = base::optional_variant< class ControllerWrap { public: - ControllerWrap(); + explicit ControllerWrap(const MTPInputPeer &peer); rpl::producer state() const; diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h index cd14698cd..a3f39f530 100644 --- a/Telegram/SourceFiles/export/export_settings.h +++ b/Telegram/SourceFiles/export/export_settings.h @@ -76,8 +76,14 @@ struct Settings { Types fullChats = DefaultFullChats(); MediaSettings media; + MTPInputPeer singlePeer = MTP_inputPeerEmpty(); + TimeId availableAt = 0; + bool onlySinglePeer() const { + return singlePeer.type() != mtpc_inputPeerEmpty; + } + static inline Types DefaultTypes() { return Type::PersonalInfo | Type::Userpics diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.cpp b/Telegram/SourceFiles/export/output/export_output_abstract.cpp index bc55dd5eb..f203a1737 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.cpp +++ b/Telegram/SourceFiles/export/output/export_output_abstract.cpp @@ -19,8 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Export { namespace Output { -QString NormalizePath(const QString &source) { - QDir folder(source); +QString NormalizePath(const Settings &settings) { + QDir folder(settings.path); const auto path = folder.absolutePath(); auto result = path.endsWith('/') ? path : (path + '/'); if (!folder.exists()) { @@ -32,7 +32,9 @@ QString NormalizePath(const QString &source) { return result; } const auto date = QDate::currentDate(); - const auto base = QString("DataExport_%1_%2_%3" + const auto base = QString(settings.onlySinglePeer() + ? "ChatExport_%1_%2_%3" + : "DataExport_%1_%2_%3" ).arg(date.day(), 2, 10, QChar('0') ).arg(date.month(), 2, 10, QChar('0') ).arg(date.year()); diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h index e764731c7..1bd7689a6 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.h +++ b/Telegram/SourceFiles/export/output/export_output_abstract.h @@ -27,7 +27,7 @@ struct Environment; namespace Output { -QString NormalizePath(const QString &source); +QString NormalizePath(const Settings &settings); struct Result; class Stats; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 36af78bc0..74b090410 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1727,7 +1727,6 @@ Result HtmlWriter::start( _settings = base::duplicate(settings); _environment = environment; _stats = stats; - _summary = fileWithRelativePath(mainFileRelativePath()); //const auto result = copyFile( // ":/export/css/bootstrap.min.css", @@ -1771,6 +1770,11 @@ Result HtmlWriter::start( } } } + + if (_settings.onlySinglePeer()) { + return Result::Success(); + } + _summary = fileWithRelativePath(mainFileRelativePath()); auto block = _summary->pushHeader("Exported Data"); block.append(_summary->pushDiv("page_body")); return _summary->writeBlock(block); @@ -1810,6 +1814,8 @@ Result HtmlWriter::writeDelayedPersonal(const QString &userpicPath) { Result HtmlWriter::writePreparedPersonal( const Data::PersonalInfo &data, const QString &userpicPath) { + Expects(_summary != nullptr); + const auto &info = data.user.info; auto userpic = UserpicData{ _selfColorIndex, kPersonalUserpicSize }; @@ -2197,11 +2203,12 @@ Result HtmlWriter::writeOtherData(const Data::File &data) { } Result HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) { - Expects(_summary != nullptr); Expects(_chats == nullptr); if (data.chats.empty() && data.left.empty()) { return Result::Success(); + } else if (_settings.onlySinglePeer()) { + return Result::Success(); } _dialogsRelativePath = "lists/chats.html"; @@ -2293,12 +2300,35 @@ Result HtmlWriter::writeDialogSlice(const Data::MessagesSlice &data) { return _chat->writeBlock(block); } -Result HtmlWriter::writeDialogEnd() { - Expects(_chats != nullptr); +Result HtmlWriter::writeEmptySinglePeer() { Expects(_chat != nullptr); + if (!_settings.onlySinglePeer() || _messagesCount != 0) { + return Result::Success(); + } + Assert(_chatFileEmpty); + if (const auto result = writeDialogOpening(0); !result) { + return result; + } + return _chat->writeBlock(_chat->pushServiceMessage( + --_dateMessageId, + _dialog, + _settings.path, + "Empty chat")); +} + +Result HtmlWriter::writeDialogEnd() { + Expects(_settings.onlySinglePeer() || _chats != nullptr); + Expects(_chat != nullptr); + + if (const auto result = writeEmptySinglePeer(); !result) { + return result; + } + if (const auto closed = base::take(_chat)->close(); !closed) { return closed; + } else if (_settings.onlySinglePeer()) { + return Result::Success(); } using Type = Data::DialogInfo::Type; @@ -2411,7 +2441,7 @@ Result HtmlWriter::writeDialogOpening(int index) { : (_dialog.name + ' ' + _dialog.lastName); auto block = _chat->pushHeader( name, - _dialogsRelativePath); + _settings.onlySinglePeer() ? QString() : _dialogsRelativePath); block.append(_chat->pushDiv("page_body chat_page")); block.append(_chat->pushDiv("history")); if (index > 0) { @@ -2489,7 +2519,11 @@ Result HtmlWriter::switchToNextChatFile(int index) { } Result HtmlWriter::finish() { - Expects(_summary != nullptr); + Expects(_settings.onlySinglePeer() || _summary != nullptr); + + if (_settings.onlySinglePeer()) { + return Result::Success(); + } auto block = QByteArray(); if (_haveSections) { @@ -2516,7 +2550,9 @@ Result HtmlWriter::copyFile( } QString HtmlWriter::mainFilePath() { - return pathWithRelativePath(mainFileRelativePath()); + return pathWithRelativePath(_settings.onlySinglePeer() + ? messagesFile(0) + : mainFileRelativePath()); } QString HtmlWriter::mainFileRelativePath() const { diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index 838c627d3..75de9c7e2 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -108,6 +108,7 @@ private: [[nodiscard]] Result validateDialogsMode(bool isLeftChannel); [[nodiscard]] Result writeDialogOpening(int index); [[nodiscard]] Result switchToNextChatFile(int index); + [[nodiscard]] Result writeEmptySinglePeer(); void pushSection( int priority, diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index d803ee092..d1ca9e96b 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -45,7 +45,7 @@ void SuggestBox::prepare() { addButton(langFactory(lng_box_ok), [=] { closeBox(); - Auth().data().startExport(); + Auth().data().startExport(Local::ReadExportSettings().singlePeer); }); addButton(langFactory(lng_export_suggest_cancel), [=] { closeBox(); }); setCloseByOutsideClick(false); @@ -121,8 +121,11 @@ void PanelController::activatePanel() { } void PanelController::createPanel() { + const auto singlePeer = _settings->onlySinglePeer(); _panel = base::make_unique_q(); - _panel->setTitle(Lang::Viewer(lng_export_title)); + _panel->setTitle(Lang::Viewer(singlePeer + ? lng_export_header_chats + : lng_export_title)); _panel->setInnerSize(st::exportPanelSize); _panel->closeRequests( ) | rpl::start_with_next([=] { @@ -154,7 +157,6 @@ void PanelController::showSettings() { settings->changes( ) | rpl::start_with_next([=](Settings &&settings) { *_settings = std::move(settings); - _saveSettingsTimer.callOnce(kSaveSettingsTimeout); }, settings->lifetime()); _panel->showInner(std::move(settings)); @@ -320,7 +322,14 @@ rpl::producer<> PanelController::stopRequests() const { }); } +void PanelController::fillParams(const PasswordCheckState &state) { + _settings->singlePeer = state.singlePeer; +} + void PanelController::updateState(State &&state) { + if (const auto start = base::get_if(&state)) { + fillParams(*start); + } if (!_panel) { createPanel(); } diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.h b/Telegram/SourceFiles/export/view/export_view_panel_controller.h index f10b19aee..6f56c8034 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.h +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.h @@ -50,6 +50,7 @@ public: ~PanelController(); private: + void fillParams(const PasswordCheckState &state); void stopExport(); void createPanel(); void updateState(State &&state); diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index 37050b426..65bf79a1f 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/fade_wrap.h" #include "platform/platform_specific.h" #include "core/file_utilities.h" +#include "auth_session.h" #include "styles/style_widgets.h" #include "styles/style_export.h" #include "styles/style_boxes.h" @@ -55,10 +56,25 @@ int SizeLimitByIndex(int index) { return megabytes() * kMegabyte; } +PeerId ReadPeerId(const MTPInputPeer &data) { + return data.match([](const MTPDinputPeerUser &data) { + return peerFromUser(data.vuser_id.v); + }, [](const MTPDinputPeerChat &data) { + return peerFromChat(data.vchat_id.v); + }, [](const MTPDinputPeerChannel &data) { + return peerFromChannel(data.vchannel_id.v); + }, [](const MTPDinputPeerSelf &data) { + return Auth().userPeerId(); + }, [](const MTPDinputPeerEmpty &data) { + return PeerId(0); + }); +} + } // namespace SettingsWidget::SettingsWidget(QWidget *parent, Settings data) : RpWidget(parent) +, _singlePeerId(ReadPeerId(data.singlePeer)) , _internal_data(std::move(data)) { setupContent(); } @@ -95,6 +111,17 @@ void SettingsWidget::setupContent() { } void SettingsWidget::setupOptions(not_null container) { + if (!_singlePeerId) { + setupFullExportOptions(container); + } + setupMediaOptions(container); + if (!_singlePeerId) { + setupOtherOptions(container); + } +} + +void SettingsWidget::setupFullExportOptions( + not_null container) { addOptionWithAbout( container, lng_export_option_info, @@ -127,38 +154,21 @@ void SettingsWidget::setupOptions(not_null container) { container, lng_export_option_public_channels, Type::PublicChannels); - - setupMediaOptions(container); - - addHeader(container, lng_export_header_other); - addOptionWithAbout( - container, - lng_export_option_sessions, - Type::Sessions, - lng_export_option_sessions_about); - addOptionWithAbout( - container, - lng_export_option_other, - Type::OtherData, - lng_export_option_other_about); } void SettingsWidget::setupMediaOptions( not_null container) { + if (_singlePeerId != 0) { + addMediaOptions(container); + return; + } const auto mediaWrap = container->add( object_ptr>( container, object_ptr(container))); const auto media = mediaWrap->entity(); addHeader(media, lng_export_header_media); - addMediaOption(media, lng_export_option_photos, MediaType::Photo); - addMediaOption(media, lng_export_option_video_files, MediaType::Video); - addMediaOption(media, lng_export_option_voice_messages, MediaType::VoiceMessage); - addMediaOption(media, lng_export_option_video_messages, MediaType::VideoMessage); - addMediaOption(media, lng_export_option_stickers, MediaType::Sticker); - addMediaOption(media, lng_export_option_gifs, MediaType::GIF); - addMediaOption(media, lng_export_option_files, MediaType::File); - addSizeSlider(media); + addMediaOptions(media); value() | rpl::map([](const Settings &data) { return data.types; @@ -178,8 +188,27 @@ void SettingsWidget::setupMediaOptions( }, mediaWrap->lifetime()); } +void SettingsWidget::setupOtherOptions( + not_null container) { + addHeader(container, lng_export_header_other); + addOptionWithAbout( + container, + lng_export_option_sessions, + Type::Sessions, + lng_export_option_sessions_about); + addOptionWithAbout( + container, + lng_export_option_other, + Type::OtherData, + lng_export_option_other_about); +} + void SettingsWidget::setupPathAndFormat( not_null container) { + if (_singlePeerId != 0) { + addLocationLabel(container); + return; + } const auto formatGroup = std::make_shared>( readData().format); formatGroup->setChangedCallback([=](Format format) { @@ -205,6 +234,7 @@ void SettingsWidget::setupPathAndFormat( void SettingsWidget::addLocationLabel( not_null container) { +#ifndef OS_MAC_STORE auto pathLabel = value() | rpl::map([](const Settings &data) { return data.path; }) | rpl::distinct_until_changed( @@ -241,6 +271,7 @@ void SettingsWidget::addLocationLabel( chooseFolder(); return false; }); +#endif // OS_MAC_STORE } not_null SettingsWidget::setupButtons( @@ -382,6 +413,30 @@ void SettingsWidget::addChatOption( } } +void SettingsWidget::addMediaOptions( + not_null container) { + addMediaOption(container, lng_export_option_photos, MediaType::Photo); + addMediaOption( + container, + lng_export_option_video_files, + MediaType::Video); + addMediaOption( + container, + lng_export_option_voice_messages, + MediaType::VoiceMessage); + addMediaOption( + container, + lng_export_option_video_messages, + MediaType::VideoMessage); + addMediaOption( + container, + lng_export_option_stickers, + MediaType::Sticker); + addMediaOption(container, lng_export_option_gifs, MediaType::GIF); + addMediaOption(container, lng_export_option_files, MediaType::File); + addSizeSlider(container); +} + void SettingsWidget::addMediaOption( not_null container, LangKey key, diff --git a/Telegram/SourceFiles/export/view/export_view_settings.h b/Telegram/SourceFiles/export/view/export_view_settings.h index 13cbc061c..193cd9689 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.h +++ b/Telegram/SourceFiles/export/view/export_view_settings.h @@ -42,7 +42,9 @@ private: not_null scroll, not_null wrap); void setupOptions(not_null container); + void setupFullExportOptions(not_null container); void setupMediaOptions(not_null container); + void setupOtherOptions(not_null container); void setupPathAndFormat(not_null container); void addHeader( not_null container, @@ -60,6 +62,7 @@ private: not_null container, LangKey key, Types types); + void addMediaOptions(not_null container); void addMediaOption( not_null container, LangKey key, @@ -76,6 +79,8 @@ private: template void changeData(Callback &&callback); + PeerId _singlePeerId = 0; + // Use through readData / changeData wrappers. Settings _internal_data; diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index b65299971..1072e4d63 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -43,6 +43,12 @@ constexpr auto kFileLoaderQueueStopTimeout = TimeMs(5000); constexpr auto kDefaultStickerInstallDate = TimeId(1); constexpr auto kProxyTypeShift = 1024; +constexpr auto kSinglePeerTypeUser = qint32(1); +constexpr auto kSinglePeerTypeChat = qint32(2); +constexpr auto kSinglePeerTypeChannel = qint32(3); +constexpr auto kSinglePeerTypeSelf = qint32(4); +constexpr auto kSinglePeerTypeEmpty = qint32(0); + using FileKey = quint64; constexpr char tdfMagic[] = { 'T', 'D', 'F', '$' }; @@ -4816,7 +4822,8 @@ void WriteExportSettings(const Export::Settings &settings) { && settings.media.sizeLimit == check.media.sizeLimit && settings.path == check.path && settings.format == check.format - && settings.availableAt == check.availableAt) { + && settings.availableAt == check.availableAt + && !settings.onlySinglePeer()) { if (_exportSettingsKey) { clearKey(_exportSettingsKey); _exportSettingsKey = 0; @@ -4830,7 +4837,8 @@ void WriteExportSettings(const Export::Settings &settings) { _writeMap(WriteMapWhen::Fast); } quint32 size = sizeof(quint32) * 6 - + Serialize::stringSize(settings.path); + + Serialize::stringSize(settings.path) + + sizeof(qint32) * 2 + sizeof(quint64); EncryptedDescriptor data(size); data.stream << quint32(settings.types) @@ -4840,6 +4848,23 @@ void WriteExportSettings(const Export::Settings &settings) { << quint32(settings.format) << settings.path << quint32(settings.availableAt); + settings.singlePeer.match([&](const MTPDinputPeerUser &user) { + data.stream + << kSinglePeerTypeUser + << qint32(user.vuser_id.v) + << quint64(user.vaccess_hash.v); + }, [&](const MTPDinputPeerChat &chat) { + data.stream << kSinglePeerTypeChat << qint32(chat.vchat_id.v); + }, [&](const MTPDinputPeerChannel &channel) { + data.stream + << kSinglePeerTypeChannel + << qint32(channel.vchannel_id.v) + << quint64(channel.vaccess_hash.v); + }, [&](const MTPDinputPeerSelf &) { + data.stream << kSinglePeerTypeSelf; + }, [&](const MTPDinputPeerEmpty &) { + data.stream << kSinglePeerTypeEmpty; + }); FileWriteDescriptor file(_exportSettingsKey); file.writeEncrypted(data); @@ -4859,6 +4884,8 @@ Export::Settings ReadExportSettings() { quint32 mediaTypes = 0, mediaSizeLimit = 0; quint32 format = 0, availableAt = 0; QString path; + qint32 singlePeerType = 0, singlePeerBareId = 0; + quint64 singlePeerAccessHash = 0; file.stream >> types >> fullChats @@ -4867,6 +4894,19 @@ Export::Settings ReadExportSettings() { >> format >> path >> availableAt; + if (!file.stream.atEnd()) { + file.stream >> singlePeerType; + switch (singlePeerType) { + case kSinglePeerTypeUser: + case kSinglePeerTypeChannel: { + file.stream >> singlePeerBareId >> singlePeerAccessHash; + } break; + case kSinglePeerTypeChat: file.stream >> singlePeerBareId; break; + case kSinglePeerTypeSelf: + case kSinglePeerTypeEmpty: break; + default: return Export::Settings(); + } + } auto result = Export::Settings(); result.types = Export::Settings::Types::from_raw(types); result.fullChats = Export::Settings::Types::from_raw(fullChats); @@ -4875,6 +4915,25 @@ Export::Settings ReadExportSettings() { result.format = Export::Output::Format(format); result.path = path; result.availableAt = availableAt; + result.singlePeer = [&] { + switch (singlePeerType) { + case kSinglePeerTypeUser: + return MTP_inputPeerUser( + MTP_int(singlePeerBareId), + MTP_long(singlePeerAccessHash)); + case kSinglePeerTypeChat: + return MTP_inputPeerChat(MTP_int(singlePeerBareId)); + case kSinglePeerTypeChannel: + return MTP_inputPeerChannel( + MTP_int(singlePeerBareId), + MTP_long(singlePeerAccessHash)); + case kSinglePeerTypeSelf: + return MTP_inputPeerSelf(); + case kSinglePeerTypeEmpty: + return MTP_inputPeerEmpty(); + } + Unexpected("Type in export data single peer."); + }(); return (file.stream.status() == QDataStream::Ok && result.validate()) ? result : Export::Settings(); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 8d8b77cb3..d993f5716 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -342,6 +342,9 @@ void Filler::addUserActions(not_null user) { lang(lng_profile_invite_to_group), [user] { AddBotToGroupBoxController::Start(user); }); } + _addAction( + lang(lng_profile_export_chat), + [=] { PeerMenuExportChat(user); }); } _addAction( lang(lng_profile_delete_conversation), @@ -364,6 +367,9 @@ void Filler::addChatActions(not_null chat) { lang(lng_profile_add_participant), [chat] { AddChatMembers(chat); }); } + _addAction( + lang(lng_profile_export_chat), + [=] { PeerMenuExportChat(chat); }); } _addAction( lang(lng_profile_clear_and_exit), @@ -398,6 +404,11 @@ void Filler::addChannelActions(not_null channel) { lang(lng_channel_add_members), [channel] { PeerMenuAddChannelMembers(channel); }); } + _addAction( + lang(isGroup + ? lng_profile_export_chat + : lng_profile_export_channel), + [=] { PeerMenuExportChat(channel); }); } if (channel->amIn()) { if (isGroup && !channel->isPublic()) { @@ -542,6 +553,10 @@ void FeedFiller::addUngroup() { } // namespace +void PeerMenuExportChat(not_null peer) { + Auth().data().startExport(peer); +} + void PeerMenuDeleteContact(not_null user) { auto text = lng_sure_delete_contact( lt_contact, diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 8baca1921..d0fe6f0de 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -44,6 +44,7 @@ void PeerMenuAddMuteAction( not_null peer, const PeerMenuCallback &addAction); +void PeerMenuExportChat(not_null peer); void PeerMenuDeleteContact(not_null user); void PeerMenuShareContactBox(not_null user); void PeerMenuAddContact(not_null user); diff --git a/Telegram/ThirdParty/crl b/Telegram/ThirdParty/crl index 9bc641f2d..527ad273b 160000 --- a/Telegram/ThirdParty/crl +++ b/Telegram/ThirdParty/crl @@ -1 +1 @@ -Subproject commit 9bc641f2d4ab140a84aea64c7f2d4669f7633246 +Subproject commit 527ad273b683d52c5adf5b45b73c6466aa0d0cf0