diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0afd120d0d..e4a5c5eff8 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -180,6 +180,8 @@ PRIVATE boxes/filters/edit_filter_box.h boxes/filters/edit_filter_chats_list.cpp boxes/filters/edit_filter_chats_list.h + boxes/filters/edit_filter_chats_preview.cpp + boxes/filters/edit_filter_chats_preview.h boxes/filters/edit_filter_links.cpp boxes/filters/edit_filter_links.h boxes/peers/add_bot_to_chat_box.cpp @@ -446,6 +448,9 @@ PRIVATE core/version.h countries/countries_manager.cpp countries/countries_manager.h + data/business/data_business_chatbots.cpp + data/business/data_business_chatbots.h + data/business/data_business_common.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp @@ -1277,6 +1282,8 @@ PRIVATE profile/profile_block_widget.h profile/profile_cover_drop_area.cpp profile/profile_cover_drop_area.h + settings/business/settings_business_exceptions.cpp + settings/business/settings_business_exceptions.h settings/business/settings_chatbots.cpp settings/business/settings_chatbots.h settings/cloud_password/settings_cloud_password_common.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 19ff138a1c..73b164deee 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2189,6 +2189,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_reply" = "Reply to Messages"; "lng_chatbots_reply_about" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."; "lng_chatbots_remove" = "Remove Bot"; +"lng_chatbots_not_found" = "Chatbot not found"; +"lng_chatbots_add" = "Add"; "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; @@ -4341,6 +4343,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_type_non_contacts" = "Non-Contacts"; "lng_filters_type_groups" = "Groups"; "lng_filters_type_channels" = "Channels"; +"lng_filters_type_new" = "New Chats"; +"lng_filters_type_existing" = "Existing Chats"; "lng_filters_type_bots" = "Bots"; "lng_filters_type_no_archived" = "Archived"; "lng_filters_type_no_muted" = "Muted"; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 37b946d637..958d014367 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/filters/edit_filter_box.h" #include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" #include "boxes/filters/edit_filter_links.h" #include "boxes/premium_limits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -56,60 +57,6 @@ using Flags = Data::ChatFilter::Flags; using ExceptionPeersRef = const base::flat_set> &; using ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const; -constexpr auto kAllTypes = { - Flag::Contacts, - Flag::NonContacts, - Flag::Groups, - Flag::Channels, - Flag::Bots, - Flag::NoMuted, - Flag::NoRead, - Flag::NoArchived, -}; - -class FilterChatsPreview final : public Ui::RpWidget { -public: - FilterChatsPreview( - not_null parent, - Flags flags, - const base::flat_set> &peers); - - [[nodiscard]] rpl::producer flagRemoved() const; - [[nodiscard]] rpl::producer> peerRemoved() const; - - void updateData( - Flags flags, - const base::flat_set> &peers); - - int resizeGetHeight(int newWidth) override; - -private: - using Button = base::unique_qptr; - struct FlagButton { - Flag flag = Flag(); - Button button; - }; - struct PeerButton { - not_null history; - Ui::PeerUserpicView userpic; - Ui::Text::String name; - Button button; - }; - - void paintEvent(QPaintEvent *e) override; - - void refresh(); - void removeFlag(Flag flag); - void removePeer(not_null history); - - std::vector _removeFlag; - std::vector _removePeer; - - rpl::event_stream _flagRemoved; - rpl::event_stream> _peerRemoved; - -}; - struct NameEditing { not_null field; bool custom = false; @@ -167,167 +114,6 @@ not_null SetupChatsPreview( return preview; } -FilterChatsPreview::FilterChatsPreview( - not_null parent, - Flags flags, - const base::flat_set> &peers) -: RpWidget(parent) { - updateData(flags, peers); -} - -void FilterChatsPreview::refresh() { - resizeToWidth(width()); -} - -void FilterChatsPreview::updateData( - Flags flags, - const base::flat_set> &peers) { - _removeFlag.clear(); - _removePeer.clear(); - const auto makeButton = [&](Fn handler) { - auto result = base::make_unique_q( - this, - st::windowFilterSmallRemove); - result->setClickedCallback(std::move(handler)); - return result; - }; - for (const auto flag : kAllTypes) { - if (flags & flag) { - _removeFlag.push_back({ - flag, - makeButton([=] { removeFlag(flag); }) }); - } - } - for (const auto &history : peers) { - _removePeer.push_back(PeerButton{ - .history = history, - .button = makeButton([=] { removePeer(history); }) - }); - } - refresh(); -} - -int FilterChatsPreview::resizeGetHeight(int newWidth) { - const auto right = st::windowFilterSmallRemoveRight; - const auto add = (st::windowFilterSmallItem.height - - st::windowFilterSmallRemove.height) / 2; - auto top = 0; - const auto moveNextButton = [&](not_null button) { - button->moveToRight(right, top + add, newWidth); - top += st::windowFilterSmallItem.height; - }; - for (const auto &[flag, button] : _removeFlag) { - moveNextButton(button.get()); - } - for (const auto &[history, userpic, name, button] : _removePeer) { - moveNextButton(button.get()); - } - return top; -} - -void FilterChatsPreview::paintEvent(QPaintEvent *e) { - auto p = Painter(this); - auto top = 0; - const auto &st = st::windowFilterSmallItem; - const auto iconLeft = st.photoPosition.x(); - const auto iconTop = st.photoPosition.y(); - const auto nameLeft = st.namePosition.x(); - p.setFont(st::windowFilterSmallItem.nameStyle.font); - const auto nameTop = st.namePosition.y(); - for (const auto &[flag, button] : _removeFlag) { - PaintFilterChatsTypeIcon( - p, - flag, - iconLeft, - top + iconTop, - width(), - st.photoSize); - - p.setPen(st::contactsNameFg); - p.drawTextLeft( - nameLeft, - top + nameTop, - width(), - FilterChatsTypeName(flag)); - top += st.height; - } - for (auto &[history, userpic, name, button] : _removePeer) { - const auto savedMessages = history->peer->isSelf(); - const auto repliesMessages = history->peer->isRepliesChat(); - if (savedMessages || repliesMessages) { - if (savedMessages) { - Ui::EmptyUserpic::PaintSavedMessages( - p, - iconLeft, - top + iconTop, - width(), - st.photoSize); - } else { - Ui::EmptyUserpic::PaintRepliesMessages( - p, - iconLeft, - top + iconTop, - width(), - st.photoSize); - } - p.setPen(st::contactsNameFg); - p.drawTextLeft( - nameLeft, - top + nameTop, - width(), - (savedMessages - ? tr::lng_saved_messages(tr::now) - : tr::lng_replies_messages(tr::now))); - } else { - history->peer->paintUserpicLeft( - p, - userpic, - iconLeft, - top + iconTop, - width(), - st.photoSize); - p.setPen(st::contactsNameFg); - if (name.isEmpty()) { - name.setText( - st::msgNameStyle, - history->peer->name(), - Ui::NameTextOptions()); - } - name.drawLeftElided( - p, - nameLeft, - top + nameTop, - button->x() - nameLeft, - width()); - } - top += st.height; - } -} - -void FilterChatsPreview::removeFlag(Flag flag) { - const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); - Assert(i != end(_removeFlag)); - _removeFlag.erase(i); - refresh(); - _flagRemoved.fire_copy(flag); -} - -void FilterChatsPreview::removePeer(not_null history) { - const auto i = ranges::find(_removePeer, history, &PeerButton::history); - Assert(i != end(_removePeer)); - _removePeer.erase(i); - refresh(); - _peerRemoved.fire_copy(history); -} - -rpl::producer FilterChatsPreview::flagRemoved() const { - return _flagRemoved.events(); -} - -rpl::producer> FilterChatsPreview::peerRemoved() const { - return _peerRemoved.events(); -} - void EditExceptions( not_null window, not_null context, diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 989a9867b3..25463f1e28 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -28,6 +28,8 @@ using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; constexpr auto kAllTypes = { + Flag::NewChats, + Flag::ExistingChats, Flag::Contacts, Flag::NonContacts, Flag::Groups, @@ -119,7 +121,7 @@ PaintRoundImageCallback TypeRow::generatePaintUserpicCallback( } Flag TypeRow::flag() const { - return static_cast(id() & 0xFF); + return static_cast(id() & 0xFFFF); } ExceptionRow::ExceptionRow(not_null history) : Row(history) { @@ -219,6 +221,8 @@ auto TypeController::rowSelectionChanges() const [[nodiscard]] QString FilterChatsTypeName(Flag flag) { switch (flag) { + case Flag::NewChats: return tr::lng_filters_type_new(tr::now); + case Flag::ExistingChats: return tr::lng_filters_type_existing(tr::now); case Flag::Contacts: return tr::lng_filters_type_contacts(tr::now); case Flag::NonContacts: return tr::lng_filters_type_non_contacts(tr::now); @@ -241,6 +245,8 @@ void PaintFilterChatsTypeIcon( int size) { const auto &color1 = [&]() -> const style::color& { switch (flag) { + case Flag::NewChats: return st::historyPeer5UserpicBg; + case Flag::ExistingChats: return st::historyPeer8UserpicBg; case Flag::Contacts: return st::historyPeer4UserpicBg; case Flag::NonContacts: return st::historyPeer7UserpicBg; case Flag::Groups: return st::historyPeer2UserpicBg; @@ -254,6 +260,8 @@ void PaintFilterChatsTypeIcon( }(); const auto &color2 = [&]() -> const style::color& { switch (flag) { + case Flag::NewChats: return st::historyPeer5UserpicBg2; + case Flag::ExistingChats: return st::historyPeer8UserpicBg2; case Flag::Contacts: return st::historyPeer4UserpicBg2; case Flag::NonContacts: return st::historyPeer7UserpicBg2; case Flag::Groups: return st::historyPeer2UserpicBg2; @@ -267,6 +275,8 @@ void PaintFilterChatsTypeIcon( }(); const auto &icon = [&]() -> const style::icon& { switch (flag) { + case Flag::NewChats: return st::windowFilterTypeNewChats; + case Flag::ExistingChats: return st::windowFilterTypeExistingChats; case Flag::Contacts: return st::windowFilterTypeContacts; case Flag::NonContacts: return st::windowFilterTypeNonContacts; case Flag::Groups: return st::windowFilterTypeGroups; @@ -469,6 +479,10 @@ object_ptr EditFilterChatsListController::prepareTypesList() { auto EditFilterChatsListController::createRow(not_null history) -> std::unique_ptr { + const auto business = _options & (Flag::NewChats | Flag::ExistingChats); + if (business && (history->peer->isSelf() || !history->peer->isUser())) { + return nullptr; + } return history->inChatList() ? std::make_unique(history) : nullptr; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp new file mode 100644 index 0000000000..3e2efb87e8 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp @@ -0,0 +1,199 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/filters/edit_filter_chats_preview.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "data/data_peer.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "ui/text/text_options.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "styles/style_chat.h" +#include "styles/style_window.h" + +namespace { + +using Flag = Data::ChatFilter::Flag; + +constexpr auto kAllTypes = { + Flag::NewChats, + Flag::ExistingChats, + Flag::Contacts, + Flag::NonContacts, + Flag::Groups, + Flag::Channels, + Flag::Bots, + Flag::NoMuted, + Flag::NoRead, + Flag::NoArchived, +}; + +} // namespace + +FilterChatsPreview::FilterChatsPreview( + not_null parent, + Flags flags, + const base::flat_set> &peers) +: RpWidget(parent) { + updateData(flags, peers); +} + +void FilterChatsPreview::refresh() { + resizeToWidth(width()); +} + +void FilterChatsPreview::updateData( + Flags flags, + const base::flat_set> &peers) { + _removeFlag.clear(); + _removePeer.clear(); + const auto makeButton = [&](Fn handler) { + auto result = base::make_unique_q( + this, + st::windowFilterSmallRemove); + result->setClickedCallback(std::move(handler)); + result->show(); + return result; + }; + for (const auto flag : kAllTypes) { + if (flags & flag) { + _removeFlag.push_back({ + flag, + makeButton([=] { removeFlag(flag); }) }); + } + } + for (const auto &history : peers) { + _removePeer.push_back(PeerButton{ + .history = history, + .button = makeButton([=] { removePeer(history); }) + }); + } + refresh(); +} + +int FilterChatsPreview::resizeGetHeight(int newWidth) { + const auto right = st::windowFilterSmallRemoveRight; + const auto add = (st::windowFilterSmallItem.height + - st::windowFilterSmallRemove.height) / 2; + auto top = 0; + const auto moveNextButton = [&](not_null button) { + button->moveToRight(right, top + add, newWidth); + top += st::windowFilterSmallItem.height; + }; + for (const auto &[flag, button] : _removeFlag) { + moveNextButton(button.get()); + } + for (const auto &[history, userpic, name, button] : _removePeer) { + moveNextButton(button.get()); + } + return top; +} + +void FilterChatsPreview::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + auto top = 0; + const auto &st = st::windowFilterSmallItem; + const auto iconLeft = st.photoPosition.x(); + const auto iconTop = st.photoPosition.y(); + const auto nameLeft = st.namePosition.x(); + p.setFont(st::windowFilterSmallItem.nameStyle.font); + const auto nameTop = st.namePosition.y(); + for (const auto &[flag, button] : _removeFlag) { + PaintFilterChatsTypeIcon( + p, + flag, + iconLeft, + top + iconTop, + width(), + st.photoSize); + + p.setPen(st::contactsNameFg); + p.drawTextLeft( + nameLeft, + top + nameTop, + width(), + FilterChatsTypeName(flag)); + top += st.height; + } + for (auto &[history, userpic, name, button] : _removePeer) { + const auto savedMessages = history->peer->isSelf(); + const auto repliesMessages = history->peer->isRepliesChat(); + if (savedMessages || repliesMessages) { + if (savedMessages) { + Ui::EmptyUserpic::PaintSavedMessages( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + } else { + Ui::EmptyUserpic::PaintRepliesMessages( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + } + p.setPen(st::contactsNameFg); + p.drawTextLeft( + nameLeft, + top + nameTop, + width(), + (savedMessages + ? tr::lng_saved_messages(tr::now) + : tr::lng_replies_messages(tr::now))); + } else { + history->peer->paintUserpicLeft( + p, + userpic, + iconLeft, + top + iconTop, + width(), + st.photoSize); + p.setPen(st::contactsNameFg); + if (name.isEmpty()) { + name.setText( + st::msgNameStyle, + history->peer->name(), + Ui::NameTextOptions()); + } + name.drawLeftElided( + p, + nameLeft, + top + nameTop, + button->x() - nameLeft, + width()); + } + top += st.height; + } +} + +void FilterChatsPreview::removeFlag(Flag flag) { + const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); + Assert(i != end(_removeFlag)); + _removeFlag.erase(i); + refresh(); + _flagRemoved.fire_copy(flag); +} + +void FilterChatsPreview::removePeer(not_null history) { + const auto i = ranges::find(_removePeer, history, &PeerButton::history); + Assert(i != end(_removePeer)); + _removePeer.erase(i); + refresh(); + _peerRemoved.fire_copy(history); +} + +rpl::producer FilterChatsPreview::flagRemoved() const { + return _flagRemoved.events(); +} + +rpl::producer> FilterChatsPreview::peerRemoved() const { + return _peerRemoved.events(); +} diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h new file mode 100644 index 0000000000..c795bc4932 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h @@ -0,0 +1,64 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_chat_filters.h" +#include "ui/rp_widget.h" +#include "ui/userpic_view.h" + +class History; + +namespace Ui { +class IconButton; +} // namespace Ui + +class FilterChatsPreview final : public Ui::RpWidget { +public: + using Flag = Data::ChatFilter::Flag; + using Flags = Data::ChatFilter::Flags; + + FilterChatsPreview( + not_null parent, + Flags flags, + const base::flat_set> &peers); + + [[nodiscard]] rpl::producer flagRemoved() const; + [[nodiscard]] rpl::producer> peerRemoved() const; + + void updateData( + Flags flags, + const base::flat_set> &peers); + + int resizeGetHeight(int newWidth) override; + +private: + using Button = base::unique_qptr; + struct FlagButton { + Flag flag = Flag(); + Button button; + }; + struct PeerButton { + not_null history; + Ui::PeerUserpicView userpic; + Ui::Text::String name; + Button button; + }; + + void paintEvent(QPaintEvent *e) override; + + void refresh(); + void removeFlag(Flag flag); + void removePeer(not_null history); + + std::vector _removeFlag; + std::vector _removePeer; + + rpl::event_stream _flagRemoved; + rpl::event_stream> _peerRemoved; + +}; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index d60cc030d1..5640c11b5a 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -982,8 +982,7 @@ bool GoodForExportFilterLink( not_null window, const Data::ChatFilter &filter) { using Flag = Data::ChatFilter::Flag; - const auto listflags = Flag::Chatlist | Flag::HasMyLinks; - if (!filter.never().empty() || (filter.flags() & ~listflags)) { + if (!filter.never().empty() || (filter.flags() & Flag::RulesMask)) { window->showToast(tr::lng_filters_link_cant(tr::now)); return false; } diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp new file mode 100644 index 0000000000..26dd216870 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -0,0 +1,34 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/business/data_business_chatbots.h" + +namespace Data { + +Chatbots::Chatbots(not_null session) +: _session(session) { +} + +Chatbots::~Chatbots() = default; + +const ChatbotsSettings &Chatbots::current() const { + return _settings.current(); +} + +rpl::producer Chatbots::changes() const { + return _settings.changes(); +} + +rpl::producer Chatbots::value() const { + return _settings.value(); +} + +void Chatbots::save(ChatbotsSettings settings) { + _settings = settings; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h new file mode 100644 index 0000000000..adfe998d2c --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -0,0 +1,44 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/business/data_business_common.h" + +class UserData; + +namespace Data { + +class Session; + +struct ChatbotsSettings { + UserData *bot = nullptr; + BusinessExceptions allowed; + BusinessExceptions disallowed; + bool repliesAllowed = false; + bool onlySelected = false; +}; + +class Chatbots final { +public: + explicit Chatbots(not_null session); + ~Chatbots(); + + [[nodiscard]] const ChatbotsSettings ¤t() const; + [[nodiscard]] rpl::producer changes() const; + [[nodiscard]] rpl::producer value() const; + + void save(ChatbotsSettings settings); + +private: + const not_null _session; + + rpl::variable _settings; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h new file mode 100644 index 0000000000..aed51fdf9a --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -0,0 +1,31 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/flags.h" + +class UserData; + +namespace Data { + +enum class BusinessChatType { + NewChats = (1 << 0), + ExistingChats = (1 << 1), + Contacts = (1 << 2), + NonContacts = (1 << 3), +}; +inline constexpr bool is_flag_type(BusinessChatType) { return true; } + +using BusinessChatTypes = base::flags; + +struct BusinessExceptions { + BusinessChatTypes types; + std::vector> list; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 7e824cc33a..237bf5c29d 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -163,6 +163,7 @@ ChatFilter ChatFilter::withTitle(const QString &title) const { ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { auto result = *this; + result._flags &= Flag::RulesMask; if (chatlist) { result._flags |= Flag::Chatlist; if (hasMyLinks) { @@ -170,8 +171,6 @@ ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { } else { result._flags &= ~Flag::HasMyLinks; } - } else { - result._flags &= ~(Flag::Chatlist | Flag::HasMyLinks); } return result; } @@ -593,7 +592,7 @@ bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) { const auto id = filter.id(); const auto exceptionsChanged = filter.always() != updated.always(); - const auto rulesMask = ~(Flag::Chatlist | Flag::HasMyLinks); + const auto rulesMask = Flag() | Flag::RulesMask; const auto rulesChanged = exceptionsChanged || ((filter.flags() & rulesMask) != (updated.flags() & rulesMask)) || (filter.never() != updated.never()); diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 987d55ebe8..7b5a964768 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -36,9 +36,13 @@ public: NoMuted = (1 << 5), NoRead = (1 << 6), NoArchived = (1 << 7), + RulesMask = ((1 << 8) - 1), Chatlist = (1 << 8), HasMyLinks = (1 << 9), + + NewChats = (1 << 10), // Telegram Business exceptions. + ExistingChats = (1 << 11), }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index e7ec132424..4efd8fa8b5 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" #include "passport/passport_form_controller.h" #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name +#include "data/business/data_business_chatbots.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" @@ -268,7 +269,8 @@ Session::Session(not_null session) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) , _stories(std::make_unique(this)) -, _savedMessages(std::make_unique(this)) { +, _savedMessages(std::make_unique(this)) +, _chatbots(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index d391d1d318..4fc7b1db1e 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -62,6 +62,7 @@ class NotifySettings; class CustomEmojiManager; class Stories; class SavedMessages; +class Chatbots; struct ReactionId; struct RepliesReadTillUpdate { @@ -142,6 +143,9 @@ public: [[nodiscard]] SavedMessages &savedMessages() const { return *_savedMessages; } + [[nodiscard]] Chatbots &chatbots() const { + return *_chatbots; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -1065,6 +1069,7 @@ private: const std::unique_ptr _customEmojiManager; const std::unique_ptr _stories; const std::unique_ptr _savedMessages; + const std::unique_ptr _chatbots; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp b/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp new file mode 100644 index 0000000000..568aca80f5 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_business_exceptions.cpp @@ -0,0 +1,145 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "settings/business/settings_business_exceptions.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" + +namespace Settings { +namespace { + +using Flag = Data::ChatFilter::Flag; +using Flags = Data::ChatFilter::Flags; + +[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) { + using Type = Data::BusinessChatType; + return ((types & Type::Contacts) ? Flag::Contacts : Flag()) + | ((types & Type::NonContacts) ? Flag::NonContacts : Flag()) + | ((types & Type::NewChats) ? Flag::NewChats : Flag()) + | ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag()); +} + +[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) { + using Type = Data::BusinessChatType; + return ((flags & Flag::Contacts) ? Type::Contacts : Type()) + | ((flags & Flag::NonContacts) ? Type::NonContacts : Type()) + | ((flags & Flag::NewChats) ? Type::NewChats : Type()) + | ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type()); +} + +} // namespace + +void EditBusinessExceptions( + not_null window, + BusinessExceptionsDescriptor &&descriptor) { + const auto session = &window->session(); + const auto options = Flag::ExistingChats + | Flag::NewChats + | Flag::Contacts + | Flag::NonContacts; + auto &&peers = descriptor.current.list | ranges::views::transform([=]( + not_null user) { + return user->owner().history(user); + }); + auto controller = std::make_unique( + session, + (descriptor.allow + ? tr::lng_filters_include_title() + : tr::lng_filters_exclude_title()), + options, + TypesToFlags(descriptor.current.types) & options, + base::flat_set>(begin(peers), end(peers)), + [=](int count) { + return nullptr; AssertIsDebug(); + }); + const auto rawController = controller.get(); + const auto save = descriptor.save; + auto initBox = [=](not_null box) { + box->setCloseByOutsideClick(false); + box->addButton(tr::lng_settings_save(), crl::guard(box, [=] { + const auto peers = box->collectSelectedRows(); + auto &&users = ranges::views::all( + peers + ) | ranges::views::transform([=](not_null peer) { + return not_null(peer->asUser()); + }) | ranges::to_vector; + save(Data::BusinessExceptions{ + .types = FlagsToTypes(rawController->chosenOptions()), + .list = std::move(users), + }); + box->closeBox(); + })); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + window->show( + Box(std::move(controller), std::move(initBox))); +} + +not_null SetupBusinessExceptionsPreview( + not_null content, + not_null*> data) { + const auto rules = data->current(); + + const auto locked = std::make_shared(); + auto &&peers = data->current().list | ranges::views::transform([=]( + not_null user) { + return user->owner().history(user); + }); + const auto preview = content->add(object_ptr( + content, + TypesToFlags(data->current().types), + base::flat_set>(begin(peers), end(peers)))); + + preview->flagRemoved( + ) | rpl::start_with_next([=](Flag flag) { + *locked = true; + *data = Data::BusinessExceptions{ + data->current().types & ~FlagsToTypes(flag), + data->current().list + }; + *locked = false; + }, preview->lifetime()); + + preview->peerRemoved( + ) | rpl::start_with_next([=](not_null history) { + auto list = data->current().list; + list.erase( + ranges::remove(list, not_null(history->peer->asUser())), + end(list)); + + *locked = true; + *data = Data::BusinessExceptions{ + data->current().types, + std::move(list) + }; + *locked = false; + }, preview->lifetime()); + + data->changes( + ) | rpl::filter([=] { + return !*locked; + }) | rpl::start_with_next([=](const Data::BusinessExceptions &rules) { + auto &&peers = rules.list | ranges::views::transform([=]( + not_null user) { + return user->owner().history(user); + }); + preview->updateData( + TypesToFlags(rules.types), + base::flat_set>(begin(peers), end(peers))); + }, preview->lifetime()); + + return preview; +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_business_exceptions.h b/Telegram/SourceFiles/settings/business/settings_business_exceptions.h new file mode 100644 index 0000000000..e60f1a01bb --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_business_exceptions.h @@ -0,0 +1,37 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/business/data_business_common.h" + +class FilterChatsPreview; + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings { + +struct BusinessExceptionsDescriptor { + Data::BusinessExceptions current; + Fn save; + bool allow = false; +}; +void EditBusinessExceptions( + not_null window, + BusinessExceptionsDescriptor &&descriptor); + +not_null SetupBusinessExceptionsPreview( + not_null content, + not_null*> data); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 34969c7d90..d358b51126 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -7,7 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" +#include "core/application.h" +#include "data/business/data_business_chatbots.h" +#include "data/data_session.h" +#include "data/data_user.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_business_exceptions.h" #include "settings/settings_common_session.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" @@ -15,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" +#include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_settings.h" @@ -29,6 +36,7 @@ public: Chatbots( QWidget *parent, not_null controller); + ~Chatbots(); [[nodiscard]] rpl::producer title() override; @@ -42,24 +50,41 @@ public: private: void setupContent(not_null controller); + void save(); void showFinished() override { _showFinished.fire({}); } + const not_null _controller; + const not_null _session; + rpl::event_stream<> _showFinished; Ui::RoundRect _bottomSkipRounding; + rpl::variable _onlySelected = false; + rpl::variable _repliesAllowed = true; + rpl::variable _allowed; + rpl::variable _disallowed; + }; Chatbots::Chatbots( QWidget *parent, not_null controller) : Section(parent) +, _controller(controller) +, _session(&controller->session()) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(controller); } +Chatbots::~Chatbots() { + if (!Core::Quitting()) { + save(); + } +} + rpl::producer Chatbots::title() { return tr::lng_chatbots_title(); } @@ -69,12 +94,12 @@ void Chatbots::setupContent( using namespace rpl::mappers; const auto content = Ui::CreateChild(this); + const auto current = controller->session().data().chatbots().current(); - struct State { - rpl::variable onlySelected = false; - rpl::variable replyAllowed = true; - }; - const auto state = content->lifetime().make_state(); + _onlySelected = current.onlySelected; + _repliesAllowed = current.repliesAllowed; + _allowed = current.allowed; + _disallowed = current.disallowed; AddDividerTextWithLottie(content, { .lottie = u"robot"_q, @@ -93,7 +118,11 @@ void Chatbots::setupContent( object_ptr( content, st::settingsChatbotsUsername, - tr::lng_chatbots_placeholder()), + tr::lng_chatbots_placeholder(), + (current.bot + ? current.bot->session().createInternalLink( + current.bot->username()) + : QString())), st::settingsChatbotsUsernameMargins); Ui::AddDividerText( @@ -104,7 +133,7 @@ void Chatbots::setupContent( Ui::AddSubsectionTitle(content, tr::lng_chatbots_access_title()); const auto group = std::make_shared( - state->onlySelected.current() ? kSelectedOnly : kAllExcept); + _onlySelected.current() ? kSelectedOnly : kAllExcept); const auto everyone = content->add( object_ptr( content, @@ -139,8 +168,18 @@ void Chatbots::setupContent( tr::lng_chatbots_exclude_button(), st::settingsChatbotsAdd, { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); + excludeAdd->setClickedCallback([=] { + EditBusinessExceptions(_controller, { + .current = _disallowed.current(), + .save = crl::guard(this, [=](Data::BusinessExceptions value) { + _disallowed = std::move(value); + }), + .allow = false, + }); + }); + SetupBusinessExceptionsPreview(excludeInner, &_disallowed); - excludeWrap->toggleOn(state->onlySelected.value() | rpl::map(!_1)); + excludeWrap->toggleOn(_onlySelected.value() | rpl::map(!_1)); excludeWrap->finishAnimating(); const auto includeWrap = content->add( @@ -157,12 +196,22 @@ void Chatbots::setupContent( tr::lng_chatbots_include_button(), st::settingsChatbotsAdd, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); + includeAdd->setClickedCallback([=] { + EditBusinessExceptions(_controller, { + .current = _allowed.current(), + .save = crl::guard(this, [=](Data::BusinessExceptions value) { + _allowed = std::move(value); + }), + .allow = true, + }); + }); + SetupBusinessExceptionsPreview(includeInner, &_allowed); - includeWrap->toggleOn(state->onlySelected.value()); + includeWrap->toggleOn(_onlySelected.value()); includeWrap->finishAnimating(); group->setChangedCallback([=](int value) { - state->onlySelected = (value == kSelectedOnly); + _onlySelected = (value == kSelectedOnly); }); Ui::AddSkip(content, st::settingsChatbotsAccessSkip); @@ -177,9 +226,9 @@ void Chatbots::setupContent( content, tr::lng_chatbots_reply(), st::settingsButtonNoIcon - ))->toggleOn(state->replyAllowed.value())->toggledChanges( + ))->toggleOn(_repliesAllowed.value())->toggledChanges( ) | rpl::start_with_next([=](bool value) { - state->replyAllowed = value; + _repliesAllowed = value; }, content->lifetime()); Ui::AddSkip(content); @@ -192,6 +241,17 @@ void Chatbots::setupContent( Ui::ResizeFitChild(this, content); } +void Chatbots::save() { + const auto settings = Data::ChatbotsSettings{ + .bot = nullptr, + .allowed = _allowed.current(), + .disallowed = _disallowed.current(), + .repliesAllowed = _repliesAllowed.current(), + .onlySelected = _onlySelected.current(), + }; + _session->data().chatbots().save(settings); +} + } // namespace Type ChatbotsId() { diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 38621c69bc..645c782858 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -303,6 +303,8 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }}; windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }}; windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }}; +windowFilterTypeNewChats: icon {{ "folders/folders_unread", historyPeerUserpicFg }}; +windowFilterTypeExistingChats: windowFilterTypeNoRead; windowFilterChatsSectionSubtitleHeight: 28px; windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) {