diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 7efcaf01a..837af84d9 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -7,12 +7,331 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_chat_filters.h" -#include "data/data_session.h" -#include "data/data_chat_filters.h" -#include "main/main_session.h" #include "apiwrap.h" +#include "boxes/peer_list_box.h" +#include "core/application.h" +#include "data/data_chat_filters.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/toasts/common_toasts.h" +#include "ui/widgets/buttons.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" namespace Api { +namespace { + +enum class ToggleAction { + Adding, + Removing, +}; + +enum class HeaderType { + AddingFilter, + AddingChats, + AllAdded, + Removing, +}; + +struct HeaderDescriptor { + base::required type; + base::required title; + int badge = 0; +}; + +class ToggleChatsController final + : public PeerListController + , public base::has_weak_ptr { +public: + ToggleChatsController( + not_null window, + ToggleAction action, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> chats); + + void prepare() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + + [[nodiscard]] auto selectedValue() const + -> rpl::producer>>; + +private: + void setupAboveWidget(); + + const not_null _window; + + ToggleAction _action = ToggleAction::Adding; + QString _slug; + FilterId _filterId = 0; + QString _filterTitle; + std::vector> _chats; + rpl::variable>> _selected; + + base::unique_qptr _menu; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] rpl::producer TitleText(HeaderType type) { + // langs + switch (type) { + case HeaderType::AddingFilter: + return rpl::single(u"Add Folder"_q); + case HeaderType::AddingChats: + return rpl::single(u"Add Chats to Folder"_q); + case HeaderType::AllAdded: + return rpl::single(u"Folder Already Added"_q); + case HeaderType::Removing: + return rpl::single(u"Remove Folder"_q); + } + Unexpected("HeaderType in TitleText."); +} + +void FillHeader( + not_null container, + HeaderDescriptor descriptor) { + // langs + const auto description = (descriptor.type == HeaderType::AddingFilter) + ? (u"Do you want to add a new chat folder "_q + + descriptor.title + + u" and join its groups and channels?"_q) + : (descriptor.type == HeaderType::AddingChats) + ? (u"Do you want to join "_q + + QString::number(descriptor.badge) + + u" chats and add them to your folder "_q + + descriptor.title + '?') + : (descriptor.type == HeaderType::AllAdded) + ? (u"You have already added the folder "_q + + descriptor.title + + u" and all its chats."_q) + : (u"Do you want to quit the chats you joined " + "when adding the folder "_q + + descriptor.title + '?'); + container->add( + object_ptr( + container, + description, + st::boxDividerLabel), + st::boxRowPadding); +} + +void ImportInvite( + base::weak_ptr weak, + const QString &slug, + const base::flat_set> &peers) { + Expects(!peers.empty()); + + const auto peer = peers.front(); + const auto api = &peer->session().api(); + const auto callback = [=](const MTPUpdates &result) { + api->applyUpdates(result); + }; + const auto error = [=](const MTP::Error &error) { + if (const auto strong = weak.get()) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { error.description() }, + }); + } + }; + auto inputs = peers | ranges::views::transform([](auto peer) { + return MTPInputPeer(peer->input); + }) | ranges::to(); + api->request(MTPcommunities_JoinCommunityInvite( + MTP_string(slug), + MTP_vector(std::move(inputs)) + )).done(callback).fail(error).send(); +} + +ToggleChatsController::ToggleChatsController( + not_null window, + ToggleAction action, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> chats) +: _window(window) +, _action(action) +, _slug(slug) +, _filterId(filterId) +, _filterTitle(title) +, _chats(std::move(chats)) { +} + +void ToggleChatsController::prepare() { + setupAboveWidget(); + auto selected = base::flat_set>(); + for (const auto &peer : _chats) { + auto row = std::make_unique(peer); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListSetRowChecked(raw, true); + selected.emplace(peer); + } + delegate()->peerListRefreshRows(); + _selected = std::move(selected); +} + +void ToggleChatsController::rowClicked(not_null row) { + const auto peer = row->peer(); + const auto checked = row->checked(); + auto selected = _selected.current(); + delegate()->peerListSetRowChecked(row, !checked); + if (checked) { + selected.remove(peer); + } else { + selected.emplace(peer); + } + _selected = std::move(selected); +} + +void ToggleChatsController::setupAboveWidget() { + using namespace Settings; + + auto wrap = object_ptr((QWidget*)nullptr); + const auto container = wrap.data(); + + const auto type = !_filterId + ? HeaderType::AddingFilter + : (_action == ToggleAction::Adding) + ? HeaderType::AddingChats + : HeaderType::Removing; + delegate()->peerListSetTitle(TitleText(type)); + FillHeader(container, { + .type = type, + .title = _filterTitle, + .badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0, + }); + + delegate()->peerListSetAboveWidget(std::move(wrap)); +} + +Main::Session &ToggleChatsController::session() const { + return _window->session(); +} + +auto ToggleChatsController::selectedValue() const +-> rpl::producer>> { + return _selected.value(); +} + +[[nodiscard]] void AlreadyFilterBox( + not_null box, + const QString &title) { + // langs + box->setTitle(TitleText(HeaderType::AllAdded)); + + FillHeader(box->verticalLayout(), { + .type = HeaderType::AllAdded, + .title = title, + }); + + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); +} + +void ProcessFilterInvite( + base::weak_ptr weak, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> peers) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + if (peers.empty()) { + if (filterId) { + strong->show(Box(AlreadyFilterBox, title)); + } else { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { tr::lng_group_invite_bad_link(tr::now) }, + }); + } + return; + } + auto controller = std::make_unique( + strong, + ToggleAction::Adding, + slug, + filterId, + title, + std::move(peers)); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + box->setStyle(st::filterInviteBox); + raw->selectedValue( + ) | rpl::start_with_next([=]( + base::flat_set> &&peers) { + const auto count = int(peers.size()); + + box->clearButtons(); + auto button = object_ptr( + box, + rpl::single(count + ? u"Add %1 Chats"_q.arg(count) + : u"Don't add chats"_q), + st::defaultActiveButton); + const auto raw = button.data(); + + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::filterInviteBox.buttonPadding; + raw->resizeToWidth(width + - padding.left() + - padding.right()); + raw->moveToLeft(padding.left(), padding.top()); + }, raw->lifetime()); + + raw->setClickedCallback([=] { + if (!count) { + box->closeBox(); + //} else if (count + alreadyInFilter() >= ...) { + // #TODO filters + } else { + ImportInvite(weak, slug, peers); + } + }); + + box->addButton(std::move(button)); + }, box->lifetime()); + }; + strong->show( + Box(std::move(controller), std::move(initBox))); +} + +void ProcessFilterInvite( + base::weak_ptr weak, + const QString &slug, + FilterId filterId, + std::vector> peers) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + const auto &list = strong->session().data().chatsFilters().list(); + const auto it = ranges::find(list, filterId, &Data::ChatFilter::id); + if (it == end(list)) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { u"Filter not found :shrug:"_q }, + }); + return; + } + ProcessFilterInvite(weak, slug, filterId, it->title(), std::move(peers)); +} + +} // namespace void SaveNewFilterPinned( not_null session, @@ -25,7 +344,70 @@ void SaveNewFilterPinned( MTP_int(filterId), filter.tl() )).send(); +} +void CheckFilterInvite( + not_null controller, + const QString &slug) { + const auto session = &controller->session(); + const auto weak = base::make_weak(controller); + session->api().checkFilterInvite(slug, [=]( + const MTPcommunities_CommunityInvite &result) { + const auto strong = weak.get(); + if (!strong) { + return; + } + auto title = QString(); + auto filterId = FilterId(); + auto peers = std::vector>(); + auto already = std::vector>(); + auto &owner = strong->session().data(); + result.match([&](const auto &data) { + owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); + }); + const auto parseList = [&](const MTPVector &list) { + auto result = std::vector>(); + result.reserve(list.v.size()); + for (const auto &peer : list.v) { + result.push_back(owner.peer(peerFromMTP(peer))); + } + return result; + }; + result.match([&](const MTPDcommunities_communityInvite &data) { + title = qs(data.vtitle()); + peers = parseList(data.vpeers()); + }, [&](const MTPDcommunities_communityInviteAlready &data) { + filterId = data.vfilter_id().v; + peers = parseList(data.vmissing_peers()); + already = parseList(data.valready_peers()); + }); + + const auto &filters = owner.chatsFilters(); + const auto notLoaded = filterId + && !ranges::contains( + owner.chatsFilters().list(), + filterId, + &Data::ChatFilter::id); + if (notLoaded) { + const auto lifetime = std::make_shared(); + owner.chatsFilters().changed( + ) | rpl::start_with_next([=] { + lifetime->destroy(); + ProcessFilterInvite(weak, slug, filterId, std::move(peers)); + }, *lifetime); + owner.chatsFilters().reload(); + } else if (filterId) { + ProcessFilterInvite(weak, slug, filterId, std::move(peers)); + } else { + ProcessFilterInvite(weak, slug, filterId, title, std::move(peers)); + } + }, [=](const MTP::Error &error) { + if (error.code() != 400) { + return; + } + ProcessFilterInvite(weak, slug, FilterId(), QString(), {}); + }); } } // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_filters.h b/Telegram/SourceFiles/api/api_chat_filters.h index 59c29f72b..99fce06a2 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.h +++ b/Telegram/SourceFiles/api/api_chat_filters.h @@ -11,10 +11,18 @@ namespace Main { class Session; } // namespace Main +namespace Window { +class SessionController; +} // namespace Window + namespace Api { void SaveNewFilterPinned( not_null session, FilterId filterId); +void CheckFilterInvite( + not_null controller, + const QString &slug); + } // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 282683741..f7a097d26 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -380,6 +380,16 @@ void ApiWrap::checkChatInvite( )).done(std::move(done)).fail(std::move(fail)).send(); } +void ApiWrap::checkFilterInvite( + const QString &slug, + FnMut done, + Fn fail) { + request(base::take(_checkFilterInviteRequestId)).cancel(); + _checkFilterInviteRequestId = request( + MTPcommunities_CheckCommunityInvite(MTP_string(slug)) + ).done(std::move(done)).fail(std::move(fail)).send(); +} + void ApiWrap::savePinnedOrder(Data::Folder *folder) { const auto &order = _session->data().pinnedChatsOrder(folder); const auto input = [](Dialogs::Key key) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index ec71c781b..a142e09a9 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -202,6 +202,10 @@ public: const QString &hash, FnMut done, Fn fail); + void checkFilterInvite( + const QString &slug, + FnMut done, + Fn fail); void processFullPeer( not_null peer, @@ -653,6 +657,7 @@ private: mtpRequestId _termsUpdateRequestId = 0; mtpRequestId _checkInviteRequestId = 0; + mtpRequestId _checkFilterInviteRequestId = 0; struct MigrateCallbacks { FnMut)> done; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 5a6a6fa72..3fa2aeeee 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -77,6 +77,14 @@ struct InviteLinkAction { } } +void ShowEmptyLinkError(not_null window) { + // langs + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(window).toastParent(), + .text = { u"Link should have at least one chat shared."_q }, + }); +} + void ChatFilterLinkBox( not_null box, Data::ChatFilterLink data) { @@ -336,6 +344,7 @@ public: void showFinished() override; [[nodiscard]] rpl::producer hasChangesValue() const; + [[nodiscard]] base::flat_set> selected() const; private: void setupAboveWidget(); @@ -347,14 +356,13 @@ private: base::flat_set> _filterChats; base::flat_set> _allowed; - rpl::variable _selected = 0; + rpl::variable>> _selected; + base::flat_set> _initial; base::unique_qptr _menu; QString _link; - Ui::RpWidget *_headerWidget = nullptr; - rpl::variable _addedHeight; rpl::variable _hasChanges = false; rpl::event_stream<> _showFinished; @@ -495,7 +503,6 @@ void LinkController::addLinkBlock(not_null container) { void LinkController::prepare() { setupAboveWidget(); - auto selected = 0; for (const auto &history : _data.chats) { const auto peer = history->peer; _allowed.emplace(peer); @@ -503,7 +510,7 @@ void LinkController::prepare() { const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); delegate()->peerListSetRowChecked(raw, true); - ++selected; + _initial.emplace(peer); } for (const auto &history : _filterChats) { if (delegate()->peerListFindRow(history->peer->id.value)) { @@ -520,14 +527,23 @@ void LinkController::prepare() { } } delegate()->peerListRefreshRows(); - _selected = selected; + _selected = _initial; } void LinkController::rowClicked(not_null row) { if (_allowed.contains(row->peer())) { + const auto peer = row->peer(); const auto checked = row->checked(); + auto selected = _selected.current(); delegate()->peerListSetRowChecked(row, !checked); - _selected = _selected.current() + (checked ? -1 : 1); + if (checked) { + selected.remove(peer); + } else { + selected.emplace(peer); + } + const auto has = (_initial != selected); + _selected = std::move(selected); + _hasChanges = has; } } @@ -546,9 +562,16 @@ void LinkController::setupAboveWidget() { addLinkBlock(container); } + // langs + auto subtitle = _selected.value( + ) | rpl::map([](const base::flat_set> &selected) { + return selected.empty() + ? u"No chats selected"_q + : (QString::number(selected.size()) + u" chats selected"_q); + }); Settings::AddSubsectionTitle( container, - rpl::single(u"3 chats selected"_q)); + std::move(subtitle)); delegate()->peerListSetAboveWidget(std::move(wrap)); } @@ -561,6 +584,10 @@ rpl::producer LinkController::hasChangesValue() const { return _hasChanges.value(); } +base::flat_set> LinkController::selected() const { + return _selected.current(); +} + LinksController::LinksController( not_null window, rpl::producer> content, @@ -804,7 +831,7 @@ void ExportFilterLink( ) | ranges::to(); session->api().request(MTPcommunities_ExportCommunityInvite( MTP_inputCommunityDialogFilter(MTP_int(id)), - MTP_string(), + MTP_string(), // title MTP_vector(std::move(mtpPeers)) )).done([=](const MTPcommunities_ExportedCommunityInvite &result) { const auto &data = result.data(); @@ -821,6 +848,34 @@ void ExportFilterLink( }).send(); } +void EditLinkChats( + const Data::ChatFilterLink &link, + base::flat_set> peers) { + Expects(!peers.empty()); + Expects(link.id != 0); + Expects(!link.url.isEmpty()); + + const auto id = link.id; + const auto front = peers.front(); + const auto session = &front->session(); + auto mtpPeers = peers | ranges::views::transform( + [](not_null peer) { return MTPInputPeer(peer->input); } + ) | ranges::to(); + session->api().request(MTPcommunities_EditExportedInvite( + MTP_flags(MTPcommunities_EditExportedInvite::Flag::f_peers), + MTP_inputCommunityDialogFilter(MTP_int(link.id)), + MTP_string(link.url), + MTPstring(), // title + MTP_vector(std::move(mtpPeers)) + )).done([=](const MTPExportedCommunityInvite &result) { + const auto &data = result.data(); + const auto link = session->data().chatsFilters().add(id, result); + //done(link); + }).fail([=](const MTP::Error &error) { + //done({ .id = id }); + }).send(); +} + object_ptr ShowLinkBox( not_null window, const Data::ChatFilter &filter, @@ -837,7 +892,12 @@ object_ptr ShowLinkBox( box->clearButtons(); if (has) { box->addButton(tr::lng_settings_save(), [=] { - + const auto chosen = raw->selected(); + if (chosen.empty()) { + ShowEmptyLinkError(window); + } else { + EditLinkChats(link, chosen); + } }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } else { diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 19fce2ad3..cf6673c44 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_authorizations.h" #include "api/api_confirm_phone.h" #include "api/api_text_entities.h" +#include "api/api_chat_filters.h" #include "api/api_chat_invite.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" @@ -78,6 +79,18 @@ bool JoinGroupByHash( return true; } +bool JoinFilterBySlug( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + Api::CheckFilterInvite(controller, match->captured(1)); + controller->window().activate(); + return true; +} + bool ShowStickerSet( Window::SessionController *controller, const Match &match, @@ -829,6 +842,10 @@ const std::vector &LocalUrlHandlers() { u"^join/?\\?invite=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, JoinGroupByHash }, + { + u"^list/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, + JoinFilterBySlug + }, { u"^(addstickers|addemoji)/?\\?set=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, ShowStickerSet @@ -953,6 +970,8 @@ QString TryConvertUrlToLocal(QString url) { return u"tg://resolve?phone="_q + phoneMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params); } else if (const auto joinChatMatch = regex_match(u"^(joinchat/|\\+|\\%20)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://join?invite="_q + url_encode(joinChatMatch->captured(2)); + } else if (const auto joinFilterMatch = regex_match(u"^(list/)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { + return u"tg://list?slug="_q + url_encode(joinFilterMatch->captured(2)); } else if (const auto stickerSetMatch = regex_match(u"^(addstickers|addemoji)/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2)); } else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 7f255f4a6..1404c5fdd 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -329,6 +329,11 @@ void ChatFilters::load() { load(false); } +void ChatFilters::reload() { + _reloading = true; + load(); +} + void ChatFilters::load(bool force) { if (_loadRequestId && !force) { return; @@ -341,6 +346,10 @@ void ChatFilters::load(bool force) { _loadRequestId = 0; }).fail([=] { _loadRequestId = 0; + if (_reloading) { + _reloading = false; + _listChanged.fire({}); + } }).send(); } @@ -372,8 +381,9 @@ void ChatFilters::received(const QVector &list) { if (!ranges::contains(begin(_list), end(_list), 0, &ChatFilter::id)) { _list.insert(begin(_list), ChatFilter()); } - if (changed || !_loaded) { + if (changed || !_loaded || _reloading) { _loaded = true; + _reloading = false; _listChanged.fire({}); } } diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index ff5e697a4..69999a124 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -112,6 +112,7 @@ public: void setPreloaded(const QVector &result); void load(); + void reload(); void apply(const MTPUpdate &update); void set(ChatFilter filter); void remove(FilterId id); @@ -175,6 +176,7 @@ private: mtpRequestId _saveOrderRequestId = 0; mtpRequestId _saveOrderAfterId = 0; bool _loaded = false; + bool _reloading = false; mtpRequestId _suggestedRequestId = 0; std::vector _suggested; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 85a982fb6..b5b304ec7 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -538,3 +538,13 @@ powerSavingButtonNoIcon: SettingsButton(powerSavingButton) { padding: margins(22px, 8px, 22px, 8px); } powerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px); + +filterInviteBox: Box(defaultBox) { + buttonPadding: margins(12px, 12px, 12px, 12px); + buttonHeight: 44px; + button: RoundButton(defaultActiveButton) { + height: 44px; + textTop: 12px; + font: font(13px semibold); + } +}