From cf1d0677d1f298a0c14b69977cfa0c9ffcd9d54f Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 18 Mar 2024 15:02:12 +0400 Subject: [PATCH] Support business bot state in chat. --- Telegram/Resources/langs/lang.strings | 8 + .../boxes/filters/edit_filter_chats_list.cpp | 9 +- .../chat_helpers/chat_helpers.style | 25 ++ .../data/business/data_business_chatbots.cpp | 70 ++++- .../data/business/data_business_chatbots.h | 15 ++ .../data/business/data_business_common.cpp | 74 +++++- .../data/business/data_business_common.h | 12 +- .../data/business/data_business_info.cpp | 4 +- Telegram/SourceFiles/data/data_peer.cpp | 17 ++ Telegram/SourceFiles/data/data_peer.h | 1 + .../SourceFiles/history/history_widget.cpp | 36 ++- Telegram/SourceFiles/history/history_widget.h | 2 + .../view/history_view_contact_status.cpp | 240 ++++++++++++++++++ .../view/history_view_contact_status.h | 37 +++ Telegram/SourceFiles/mtproto/scheme/api.tl | 8 +- .../business/settings_away_message.cpp | 1 + .../settings/business/settings_chatbots.cpp | 1 + .../settings/business/settings_greeting.cpp | 1 + .../business/settings_recipients_helper.cpp | 76 ++++-- .../business/settings_recipients_helper.h | 3 + 20 files changed, 588 insertions(+), 52 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6b501c4dba..5aeac265d4 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2288,6 +2288,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_not_found" = "Chatbot not found."; "lng_chatbots_add" = "Add"; "lng_chatbots_info_url" = "https://telegram.org/privacy"; +"lng_chatbot_status_can_reply" = "bot manages this chat"; +"lng_chatbot_status_paused" = "bot stopped"; +"lng_chatbot_status_views" = "bot has access to this chat"; +"lng_chatbot_button_pause" = "Stop"; +"lng_chatbot_button_resume" = "Start"; +"lng_chatbot_menu_manage" = "Manage bot"; +"lng_chatbot_menu_remove" = "Remove bot from this chat"; +"lng_chatbot_menu_revoke" = "Revoke access to this chat"; "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 0ee2bace04..dd694585c5 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -351,9 +351,9 @@ Main::Session &EditFilterChatsListController::session() const { } int EditFilterChatsListController::selectedTypesCount() const { - Expects(_chatlist || _typesDelegate != nullptr); + Expects(_chatlist || !_options || _typesDelegate != nullptr); - if (_chatlist) { + if (_chatlist || !_options) { return 0; } auto result = 0; @@ -396,7 +396,7 @@ bool EditFilterChatsListController::handleDeselectForeignRow( void EditFilterChatsListController::prepareViewHook() { delegate()->peerListSetTitle(std::move(_title)); - if (!_chatlist) { + if (!_chatlist && _options) { delegate()->peerListSetAboveWidget(prepareTypesList()); } @@ -479,7 +479,8 @@ object_ptr EditFilterChatsListController::prepareTypesList() { auto EditFilterChatsListController::createRow(not_null history) -> std::unique_ptr { - const auto business = _options & (Flag::NewChats | Flag::ExistingChats); + const auto business = (_options & (Flag::NewChats | Flag::ExistingChats)) + || (!_options && !_chatlist); if (business && (history->peer->isSelf() || !history->peer->isUser())) { return nullptr; } diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index a622c37eaf..d5a443d8e6 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -857,6 +857,31 @@ historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) { } historyContactStatusMinSkip: 16px; +historyBusinessBotPhoto: UserpicButton(defaultUserpicButton) { + size: size(46px, 46px); + photoSize: 46px; + photoPosition: point(0px, 0px); +} +historyBusinessBotName: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; +} +historyBusinessBotStatus: FlatLabel(historyContactStatusLabel) { + textFg: windowSubTextFg; +} +historyBusinessBotToggle: defaultActiveButton; +historyBusinessBotSettings: IconButton(defaultIconButton) { + icon: icon{{ "menu/customize", menuIconFg }}; + iconOver: icon{{ "menu/customize", menuIconFgOver }}; + iconPosition: point(-1px, -1px); + rippleAreaSize: 40px; + rippleAreaPosition: point(4px, 9px); + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } + height: 58px; + width: 48px; +} + historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }}; historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }}; diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index 5c8c9f8957..4cf252bc94 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -87,7 +87,7 @@ void Chatbots::save( ? Flag::f_can_reply : Flag()), (settings.bot ? settings.bot : was.bot)->inputUser, - ToMTP(settings.recipients) + ForBotsToMTP(settings.recipients) )).done([=](const MTPUpdates &result) { api->applyUpdates(result); if (done) { @@ -103,4 +103,72 @@ void Chatbots::save( _settings = settings; } +void Chatbots::togglePaused(not_null peer, bool paused) { + const auto type = paused + ? SentRequestType::Pause + : SentRequestType::Unpause; + const auto api = &_owner->session().api(); + const auto i = _sentRequests.find(peer); + if (i != end(_sentRequests)) { + const auto already = i->second.type; + if (already == SentRequestType::Remove || already == type) { + return; + } + api->request(i->second.requestId).cancel(); + _sentRequests.erase(i); + } + const auto id = api->request(MTPaccount_ToggleConnectedBotPaused( + peer->input, + MTP_bool(paused) + )).done([=] { + if (_sentRequests[peer].type != type) { + return; + } else if (const auto settings = peer->barSettings()) { + peer->setBarSettings(paused + ? (*settings | PeerBarSetting::BusinessBotPaused) + : (*settings & ~PeerBarSetting::BusinessBotPaused)); + } else { + api->requestPeerSettings(peer); + } + _sentRequests.remove(peer); + }).fail([=] { + if (_sentRequests[peer].type != type) { + return; + } + api->requestPeerSettings(peer); + _sentRequests.remove(peer); + }).send(); + _sentRequests[peer] = SentRequest{ type, id }; +} + +void Chatbots::removeFrom(not_null peer) { + const auto type = SentRequestType::Remove; + const auto api = &_owner->session().api(); + const auto i = _sentRequests.find(peer); + if (i != end(_sentRequests)) { + const auto already = i->second.type; + if (already == type) { + return; + } + api->request(i->second.requestId).cancel(); + _sentRequests.erase(i); + } + const auto id = api->request(MTPaccount_DisablePeerConnectedBot( + peer->input + )).done([=] { + if (_sentRequests[peer].type != type) { + return; + } else if (const auto settings = peer->barSettings()) { + peer->clearBusinessBot(); + } else { + api->requestPeerSettings(peer); + } + _sentRequests.remove(peer); + }).fail([=] { + api->requestPeerSettings(peer); + _sentRequests.remove(peer); + }).send(); + _sentRequests[peer] = SentRequest{ type, id }; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index 6328b487d4..27befd6dfe 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -41,13 +41,28 @@ public: Fn done, Fn fail); + void togglePaused(not_null peer, bool paused); + void removeFrom(not_null peer); + private: + enum class SentRequestType { + Pause, + Unpause, + Remove, + }; + struct SentRequest { + SentRequestType type = SentRequestType::Pause; + mtpRequestId requestId = 0; + }; + const not_null _owner; rpl::variable _settings; mtpRequestId _requestId = 0; bool _loaded = false; + base::flat_map, SentRequest> _sentRequests; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 06cb17e91e..7fc092115d 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -51,16 +51,13 @@ constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax; return intervals; } -} // namespace - -MTPInputBusinessRecipients ToMTP( - const BusinessRecipients &data) { - using Flag = MTPDinputBusinessRecipients::Flag; - using Type = BusinessChatType; +template +auto RecipientsFlags(const BusinessRecipients &data) { const auto &chats = data.allButExcluded ? data.excluded : data.included; - const auto flags = Flag() + using Type = BusinessChatType; + return Flag() | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) | ((chats.types & Type::ExistingChats) ? Flag::f_existing_chats @@ -69,12 +66,30 @@ MTPInputBusinessRecipients ToMTP( | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) | (chats.list.empty() ? Flag() : Flag::f_users) | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); - const auto &users = data.allButExcluded - ? data.excluded - : data.included; +} + +} // namespace + +MTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) { + using Flag = MTPDinputBusinessRecipients::Flag; + const auto &chats = data.allButExcluded ? data.excluded : data.included; return MTP_inputBusinessRecipients( - MTP_flags(flags), - MTP_vector_from_range(users.list + MTP_flags(RecipientsFlags(data)), + MTP_vector_from_range(chats.list + | ranges::views::transform(&UserData::inputUser))); +} + +MTPInputBusinessBotRecipients ForBotsToMTP(const BusinessRecipients &data) { + using Flag = MTPDinputBusinessBotRecipients::Flag; + const auto &chats = data.allButExcluded ? data.excluded : data.included; + return MTP_inputBusinessBotRecipients( + MTP_flags(RecipientsFlags(data) + | ((data.allButExcluded || data.excluded.empty()) + ? Flag() + : Flag::f_exclude_users)), + MTP_vector_from_range(chats.list + | ranges::views::transform(&UserData::inputUser)), + MTP_vector_from_range(data.excluded.list | ranges::views::transform(&UserData::inputUser))); } @@ -103,7 +118,40 @@ BusinessRecipients FromMTP( return result; } -[[nodiscard]] BusinessDetails FromMTP( +BusinessRecipients FromMTP( + not_null owner, + const MTPBusinessBotRecipients &recipients) { + using Type = BusinessChatType; + + const auto &data = recipients.data(); + auto result = BusinessRecipients{ + .allButExcluded = data.is_exclude_selected(), + }; + auto &chats = result.allButExcluded + ? result.excluded + : result.included; + chats.types = Type() + | (data.is_new_chats() ? Type::NewChats : Type()) + | (data.is_existing_chats() ? Type::ExistingChats : Type()) + | (data.is_contacts() ? Type::Contacts : Type()) + | (data.is_non_contacts() ? Type::NonContacts : Type()); + if (const auto users = data.vusers()) { + for (const auto &userId : users->v) { + chats.list.push_back(owner->user(UserId(userId.v))); + } + } + if (!result.allButExcluded) { + if (const auto excluded = data.vexclude_users()) { + for (const auto &userId : excluded->v) { + result.excluded.list.push_back( + owner->user(UserId(userId.v))); + } + } + } + return result; +} + +BusinessDetails FromMTP( const tl::conditional &hours, const tl::conditional &location) { auto result = BusinessDetails(); diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index af34294210..3793f0b607 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -49,11 +49,21 @@ struct BusinessRecipients { const BusinessRecipients &b) = default; }; -[[nodiscard]] MTPInputBusinessRecipients ToMTP( +enum class BusinessRecipientsType : uchar { + Messages, + Bots, +}; + +[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP( + const BusinessRecipients &data); +[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP( const BusinessRecipients &data); [[nodiscard]] BusinessRecipients FromMTP( not_null owner, const MTPBusinessRecipients &recipients); +[[nodiscard]] BusinessRecipients FromMTP( + not_null owner, + const MTPBusinessBotRecipients &recipients); struct Timezone { QString id; diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 151e36dcba..1e7beef884 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -49,14 +49,14 @@ namespace { MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()), MTP_int(data.shortcutId), ToMTP(data.schedule), - ToMTP(data.recipients)); + ForMessagesToMTP(data.recipients)); } [[nodiscard]] MTPInputBusinessGreetingMessage ToMTP( const GreetingSettings &data) { return MTP_inputBusinessGreetingMessage( MTP_int(data.shortcutId), - ToMTP(data.recipients), + ForMessagesToMTP(data.recipients), MTP_int(data.noActivityDays)); } diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 00cb5fd2de..9bf563f6c1 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -608,6 +608,23 @@ void PeerData::checkFolder(FolderId folderId) { } } +void PeerData::clearBusinessBot() { + if (const auto details = _barDetails.get()) { + if (details->requestChatDate) { + details->businessBot = nullptr; + details->businessBotManageUrl = QString(); + } else { + _barDetails = nullptr; + } + } + if (const auto settings = barSettings()) { + setBarSettings(*settings + & ~PeerBarSetting::BusinessBotPaused + & ~PeerBarSetting::BusinessBotCanReply + & ~PeerBarSetting::HasBusinessBot); + } +} + void PeerData::setTranslationDisabled(bool disabled) { const auto flag = disabled ? TranslationFlag::Disabled diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index ab309ec1bb..bf7d4e0255 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -373,6 +373,7 @@ public: [[nodiscard]] TimeId requestChatDate() const; [[nodiscard]] UserData *businessBot() const; [[nodiscard]] QString businessBotManageUrl() const; + void clearBusinessBot(); enum class TranslationFlag : uchar { Unknown, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index e24f131ee2..cec389eb88 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1578,6 +1578,9 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { void HistoryWidget::orderWidgets() { _voiceRecordBar->raise(); _send->raise(); + if (_businessBotStatus) { + _businessBotStatus->bar().raise(); + } if (_contactStatus) { _contactStatus->bar().raise(); } @@ -2282,17 +2285,30 @@ void HistoryWidget::showHistory( _showAtMsgHighlightPartOffsetHint = highlightPartOffsetHint; _historyInited = false; _contactStatus = nullptr; + _businessBotStatus = nullptr; if (peerId) { + using namespace HistoryView; _peer = session().data().peer(peerId); - _contactStatus = std::make_unique( + _contactStatus = std::make_unique( controller(), this, _peer, false); - _contactStatus->bar().heightValue() | rpl::start_with_next([=] { + _contactStatus->bar().heightValue( + ) | rpl::start_with_next([=] { updateControlsGeometry(); }, _contactStatus->bar().lifetime()); + if (const auto user = _peer->asUser()) { + _businessBotStatus = std::make_unique( + controller(), + this, + user); + _businessBotStatus->bar().heightValue( + ) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _businessBotStatus->bar().lifetime()); + } orderWidgets(); controller()->tabbedSelector()->setCurrentPeer(_peer); } @@ -2879,6 +2895,9 @@ void HistoryWidget::updateControlsVisibility() { if (_contactStatus) { _contactStatus->show(); } + if (_businessBotStatus) { + _businessBotStatus->show(); + } if (isChoosingTheme() || (!editingMessage() && (isSearching() @@ -4059,6 +4078,9 @@ void HistoryWidget::hideChildWidgets() { if (_contactStatus) { _contactStatus->hide(); } + if (_businessBotStatus) { + _businessBotStatus->hide(); + } hideChildren(); } @@ -5840,7 +5862,11 @@ void HistoryWidget::updateControlsGeometry() { if (_contactStatus) { _contactStatus->bar().move(0, contactStatusTop); } - const auto scrollAreaTop = contactStatusTop + (_contactStatus ? _contactStatus->bar().height() : 0); + const auto businessBotTop = contactStatusTop + (_contactStatus ? _contactStatus->bar().height() : 0); + if (_businessBotStatus) { + _businessBotStatus->bar().move(0, businessBotTop); + } + const auto scrollAreaTop = businessBotTop + (_businessBotStatus ? _businessBotStatus->bar().height() : 0); if (_scroll->y() != scrollAreaTop) { _scroll->moveToLeft(0, scrollAreaTop); _fieldAutocomplete->setBoundings(_scroll->geometry()); @@ -6076,6 +6102,9 @@ void HistoryWidget::updateHistoryGeometry( if (_contactStatus) { newScrollHeight -= _contactStatus->bar().height(); } + if (_businessBotStatus) { + newScrollHeight -= _businessBotStatus->bar().height(); + } if (isChoosingTheme()) { newScrollHeight -= _chooseTheme->height(); } else if (!editingMessage() @@ -6457,6 +6486,7 @@ int HistoryWidget::computeMaxFieldHeight() const { const auto available = height() - _topBar->height() - (_contactStatus ? _contactStatus->bar().height() : 0) + - (_businessBotStatus ? _businessBotStatus->bar().height() : 0) - (_pinnedBar ? _pinnedBar->height() : 0) - (_groupCallBar ? _groupCallBar->height() : 0) - (_requestsBar ? _requestsBar->height() : 0) diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index a61e74fff1..43966331e8 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -89,6 +89,7 @@ namespace HistoryView { class StickerToast; class TopBarWidget; class ContactStatus; +class BusinessBotStatus; class Element; class PinnedTracker; class TranslateBar; @@ -744,6 +745,7 @@ private: bool _isInlineBot = false; std::unique_ptr _contactStatus; + std::unique_ptr _businessBotStatus; const std::shared_ptr _send; object_ptr _unblock; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 5403cc6569..6eb5d8e405 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -8,9 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_contact_status.h" #include "lang/lang_keys.h" +#include "ui/controls/userpic_button.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/padding_wrap.h" #include "ui/layers/generic_box.h" #include "ui/toast/toast.h" @@ -18,7 +21,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/boxes/confirm_box.h" #include "ui/layers/generic_box.h" +#include "core/click_handler_types.h" #include "core/ui_integration.h" +#include "data/business/data_business_chatbots.h" #include "data/notify/data_notify_settings.h" #include "data/data_peer.h" #include "data/data_user.h" @@ -839,6 +844,241 @@ void ContactStatus::hide() { _bar.hide(); } +class BusinessBotStatus::Bar final : public Ui::RpWidget { +public: + Bar(QWidget *parent); + + void showState(State state); + + [[nodiscard]] rpl::producer<> pauseClicks() const; + [[nodiscard]] rpl::producer<> resumeClicks() const; + [[nodiscard]] rpl::producer<> removeClicks() const; + [[nodiscard]] rpl::producer<> manageClicks() const; + +private: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + + void showMenu(); + + object_ptr _userpic = { nullptr }; + object_ptr _name; + object_ptr _status; + object_ptr _togglePaused; + object_ptr _settings; + rpl::event_stream<> _removeClicks; + rpl::event_stream<> _manageClicks; + base::unique_qptr _menu; + bool _paused = false; + +}; + +BusinessBotStatus::Bar::Bar(QWidget *parent) +: RpWidget(parent) +, _name(this, st::historyBusinessBotName) +, _status(this, st::historyBusinessBotStatus) +, _togglePaused( + this, + rpl::single(QString()), + st::historyBusinessBotToggle) +, _settings(this, st::historyBusinessBotSettings) { + _name->setAttribute(Qt::WA_TransparentForMouseEvents); + _status->setAttribute(Qt::WA_TransparentForMouseEvents); + _settings->setClickedCallback([=] { + showMenu(); + }); +} + +void BusinessBotStatus::Bar::showState(State state) { + Expects(state.bot != nullptr); + + _userpic = object_ptr( + this, + state.bot, + st::historyBusinessBotPhoto); + _userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + _userpic->show(); + _name->setText(state.bot->name()); + _status->setText(!state.canReply + ? tr::lng_chatbot_status_views(tr::now) + : state.paused + ? tr::lng_chatbot_status_paused(tr::now) + : tr::lng_chatbot_status_can_reply(tr::now)); + _togglePaused->setText(state.paused + ? tr::lng_chatbot_button_resume() + : tr::lng_chatbot_button_pause()); + _togglePaused->setVisible(state.canReply); + _paused = state.paused; + resizeToWidth(width()); +} + +rpl::producer<> BusinessBotStatus::Bar::pauseClicks() const { + return _togglePaused->clicks() | rpl::filter([=] { + return !_paused; + }) | rpl::to_empty; +} + +rpl::producer<> BusinessBotStatus::Bar::resumeClicks() const { + return _togglePaused->clicks() | rpl::filter([=] { + return _paused; + }) | rpl::to_empty; +} + +rpl::producer<> BusinessBotStatus::Bar::removeClicks() const { + return _removeClicks.events(); +} + +rpl::producer<> BusinessBotStatus::Bar::manageClicks() const { + return _manageClicks.events(); +} + +void BusinessBotStatus::Bar::showMenu() { + if (_menu) { + return; + } + _menu = base::make_unique_q( + this, + st::popupMenuExpandedSeparator); + _menu->setDestroyedCallback([ + weak = Ui::MakeWeak(this), + weakButton = Ui::MakeWeak(_settings.data()), + menu = _menu.get()] { + if (weak && weak->_menu == menu) { + if (weakButton) { + weakButton->setForceRippled(false); + } + } + }); + _settings->setForceRippled(true); + + const auto addAction = Ui::Menu::CreateAddActionCallback(_menu); + + addAction(tr::lng_chatbot_menu_manage(tr::now), crl::guard(this, [=] { + _manageClicks.fire({}); + }), &st::menuIconSettings); + addAction({ + .text = (_togglePaused->isHidden() + ? tr::lng_chatbot_menu_revoke(tr::now) + : tr::lng_chatbot_menu_remove(tr::now)), + .handler = crl::guard(this, [=] { _removeClicks.fire({}); }), + .icon = &st::menuIconDisableAttention, + .isAttention = true, + }); + + _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + _menu->popup(mapToGlobal(QPoint( + width() + st::topBarMenuPosition.x(), + st::topBarMenuPosition.y()))); +} + +void BusinessBotStatus::Bar::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(e->rect(), st::historyContactStatusButton.bgColor); +} + +int BusinessBotStatus::Bar::resizeGetHeight(int newWidth) { + const auto &st = st::defaultPeerList.item; + _settings->moveToRight(0, 0, newWidth); + if (_userpic) { + _userpic->moveToLeft(st.photoPosition.x(), st.photoPosition.y()); + } + auto available = newWidth - _settings->width() - st.namePosition.x(); + if (!_togglePaused->isHidden()) { + _togglePaused->moveToRight(_settings->width(), 0); + available -= _togglePaused->width(); + } + _name->resizeToWidth(available); + _name->moveToLeft(st.namePosition.x(), st.namePosition.y()); + _status->resizeToWidth(available); + _status->moveToLeft(st.statusPosition.x(), st.statusPosition.y()); + return st.height; +} + +BusinessBotStatus::BusinessBotStatus( + not_null window, + not_null parent, + not_null peer) +: _controller(window) +, _inner(Ui::CreateChild(parent.get())) +, _bar(parent, object_ptr::fromRaw(_inner)) { + setupState(peer); + setupHandlers(peer); +} + +auto BusinessBotStatus::PeerState(not_null peer) +-> rpl::producer { + using SettingsChange = PeerData::BarSettings::Change; + return peer->barSettingsValue( + ) | rpl::map([=](SettingsChange settings) -> State { + using Flag = PeerBarSetting; + return { + .bot = peer->businessBot(), + .manageUrl = peer->businessBotManageUrl(), + .canReply = ((settings.value & Flag::BusinessBotCanReply) != 0), + .paused = ((settings.value & Flag::BusinessBotPaused) != 0), + }; + }); +} + +void BusinessBotStatus::setupState(not_null peer) { + if (!BarCurrentlyHidden(peer)) { + peer->session().api().requestPeerSettings(peer); + } + PeerState( + peer + ) | rpl::start_with_next([=](State state) { + _state = state; + if (!state.bot) { + _bar.toggleContent(false); + } else { + _inner->showState(state); + _bar.toggleContent(true); + } + }, _bar.lifetime()); +} + +void BusinessBotStatus::setupHandlers(not_null peer) { + _inner->pauseClicks( + ) | rpl::start_with_next([=] { + peer->owner().chatbots().togglePaused(peer, true); + }, _bar.lifetime()); + + _inner->resumeClicks( + ) | rpl::start_with_next([=] { + peer->owner().chatbots().togglePaused(peer, false); + }, _bar.lifetime()); + + _inner->removeClicks( + ) | rpl::start_with_next([=] { + peer->owner().chatbots().removeFrom(peer); + }, _bar.lifetime()); + + _inner->manageClicks( + ) | rpl::start_with_next([=] { + UrlClickHandler::Open( + _state.manageUrl, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = base::make_weak(_controller), + .botStartAutoSubmit = true, + })); + }, _bar.lifetime()); +} + +void BusinessBotStatus::show() { + if (!_shown) { + _shown = true; + if (_state.bot) { + _inner->showState(_state); + _bar.toggleContent(true); + } + } + _bar.show(); +} + +void BusinessBotStatus::hide() { + _bar.hide(); +} + TopicReopenBar::TopicReopenBar( not_null parent, not_null topic) diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.h b/Telegram/SourceFiles/history/view/history_view_contact_status.h index 076eeeefc7..aafc23a6a7 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.h +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.h @@ -124,6 +124,43 @@ private: }; +class BusinessBotStatus final { +public: + BusinessBotStatus( + not_null controller, + not_null parent, + not_null peer); + + void show(); + void hide(); + + [[nodiscard]] SlidingBar &bar() { + return _bar; + } + +private: + class Bar; + + struct State { + UserData *bot = nullptr; + QString manageUrl; + bool canReply = false; + bool paused = false; + }; + + void setupState(not_null peer); + void setupHandlers(not_null peer); + + static rpl::producer PeerState(not_null peer); + + const not_null _controller; + State _state; + QPointer _inner; + SlidingBar _bar; + bool _shown = false; + +}; + class TopicReopenBar final { public: TopicReopenBar( diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index de9325fa51..a0146f1bce 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1704,7 +1704,7 @@ inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; messages.quickReplies#c68d6695 quick_replies:Vector messages:Vector chats:Vector users:Vector = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; -connectedBot#e7e999e7 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessRecipients = ConnectedBot; +connectedBot#bd068601 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessBotRecipients = ConnectedBot; account.connectedBots#17d7f87b connected_bots:Vector users:Vector = account.ConnectedBots; @@ -1723,6 +1723,10 @@ inputCollectiblePhone#a2e214a4 phone:string = InputCollectible; fragment.collectibleInfo#6ebdff91 purchase_date:int currency:string amount:long crypto_currency:string crypto_amount:long url:string = fragment.CollectibleInfo; +inputBusinessBotRecipients#c4e5921e flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector exclude_users:flags.6?Vector = InputBusinessBotRecipients; + +businessBotRecipients#b88cf373 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector exclude_users:flags.6?Vector = BusinessBotRecipients; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1853,7 +1857,7 @@ account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?Bus account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; -account.updateConnectedBot#9c2d527d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessRecipients = Updates; +account.updateConnectedBot#43d8521d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessBotRecipients = Updates; account.getConnectedBots#4ea4c80f = account.ConnectedBots; account.getBotBusinessConnection#76a86270 connection_id:string = Updates; account.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool; diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index a5fef26a22..0caa86b349 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -336,6 +336,7 @@ void AwayMessage::setupContent( .controller = controller, .title = tr::lng_away_recipients(), .data = &_recipients, + .type = Data::BusinessRecipientsType::Messages, }); Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 656d499581..5b59d4987c 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -448,6 +448,7 @@ void Chatbots::setupContent( .controller = controller, .title = tr::lng_chatbots_access_title(), .data = &_recipients, + .type = Data::BusinessRecipientsType::Bots, }); Ui::AddSkip(content, st::settingsChatbotsAccessSkip); diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 9809c1b719..052e97a396 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -229,6 +229,7 @@ void Greeting::setupContent( .controller = controller, .title = tr::lng_greeting_recipients(), .data = &_recipients, + .type = Data::BusinessRecipientsType::Messages, }); Ui::AddSkip(inner); diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp index 160b1d83b8..76d7631e79 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -69,7 +69,7 @@ void EditBusinessChats( (descriptor.include ? tr::lng_filters_include_title() : tr::lng_filters_exclude_title()), - options, + (descriptor.usersOnly ? Flag() : options), TypesToFlags(descriptor.current.types) & options, base::flat_set>(begin(peers), end(peers)), 100, @@ -162,6 +162,8 @@ void AddBusinessRecipientsSelector( auto &lifetime = container->lifetime(); const auto controller = descriptor.controller; const auto data = descriptor.data; + const auto includeWithExcluded = (descriptor.type + == Data::BusinessRecipientsType::Bots); const auto change = [=](Fn modify) { auto now = data->current(); modify(now); @@ -191,11 +193,17 @@ void AddBusinessRecipientsSelector( Ui::AddSkip(container, st::settingsChatbotsAccessSkip); Ui::AddDivider(container); + const auto includeWrap = container->add( + object_ptr>( + container, + object_ptr(container)) + )->setDuration(0); const auto excludeWrap = container->add( object_ptr>( container, object_ptr(container)) )->setDuration(0); + const auto excludeInner = excludeWrap->entity(); Ui::AddSkip(excludeInner); @@ -205,18 +213,34 @@ void AddBusinessRecipientsSelector( tr::lng_chatbots_exclude_button(), st::settingsChatbotsAdd, { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); - excludeAdd->setClickedCallback([=] { + const auto addExcluded = [=] { const auto save = [=](Data::BusinessChats value) { change([&](Data::BusinessRecipients &data) { + if (includeWithExcluded) { + if (!data.allButExcluded) { + value.types = {}; + } + for (const auto &user : value.list) { + data.included.list.erase( + ranges::remove(data.included.list, user), + end(data.included.list)); + } + } + if (!value.empty()) { + data.included = {}; + } data.excluded = std::move(value); }); }; EditBusinessChats(controller, { .current = data->current().excluded, .save = crl::guard(excludeAdd, save), + .usersOnly = (includeWithExcluded + && !data->current().allButExcluded), .include = false, }); - }); + }; + excludeAdd->setClickedCallback(addExcluded); const auto excluded = lifetime.make_state< rpl::variable @@ -227,24 +251,19 @@ void AddBusinessRecipientsSelector( }, lifetime); excluded->changes( ) | rpl::start_with_next([=](Data::BusinessChats &&value) { - auto now = data->current(); - now.excluded = std::move(value); - *data = std::move(now); + change([&](Data::BusinessRecipients &data) { + data.excluded = std::move(value); + }); }, lifetime); SetupBusinessChatsPreview(excludeInner, excluded); excludeWrap->toggleOn(data->value( - ) | rpl::map([](const Data::BusinessRecipients &value) { - return value.allButExcluded; + ) | rpl::map([=](const Data::BusinessRecipients &value) { + return value.allButExcluded || includeWithExcluded; })); excludeWrap->finishAnimating(); - const auto includeWrap = container->add( - object_ptr>( - container, - object_ptr(container)) - )->setDuration(0); const auto includeInner = includeWrap->entity(); Ui::AddSkip(includeInner); @@ -254,18 +273,32 @@ void AddBusinessRecipientsSelector( tr::lng_chatbots_include_button(), st::settingsChatbotsAdd, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); - includeAdd->setClickedCallback([=] { + const auto addIncluded = [=] { const auto save = [=](Data::BusinessChats value) { change([&](Data::BusinessRecipients &data) { + if (includeWithExcluded) { + for (const auto &user : value.list) { + data.excluded.list.erase( + ranges::remove(data.excluded.list, user), + end(data.excluded.list)); + } + } + if (!value.empty()) { + data.excluded.types = {}; + } data.included = std::move(value); }); + if (!data->current().included.empty()) { + group->setValue(kSelectedOnly); + } }; EditBusinessChats(controller, { - .current = data->current().included , + .current = data->current().included, .save = crl::guard(includeAdd, save), .include = true, }); - }); + }; + includeAdd->setClickedCallback(addIncluded); const auto included = lifetime.make_state< rpl::variable @@ -298,16 +331,7 @@ void AddBusinessRecipientsSelector( group->setChangedCallback([=](int value) { if (value == kSelectedOnly && data->current().included.empty()) { group->setValue(kAllExcept); - const auto save = [=](Data::BusinessChats value) { - change([&](Data::BusinessRecipients &data) { - data.included = std::move(value); - }); - group->setValue(kSelectedOnly); - }; - EditBusinessChats(controller, { - .save = crl::guard(includeAdd, save), - .include = true, - }); + addIncluded(); return; } change([&](Data::BusinessRecipients &data) { diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h index f4432ea3bb..8be1d42dc5 100644 --- a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/required.h" #include "data/business/data_business_common.h" #include "settings/settings_common_session.h" @@ -52,6 +53,7 @@ private: struct BusinessChatsDescriptor { Data::BusinessChats current; Fn save; + bool usersOnly = false; bool include = false; }; void EditBusinessChats( @@ -66,6 +68,7 @@ struct BusinessRecipientsSelectorDescriptor { not_null controller; rpl::producer title; not_null*> data; + base::required type; }; void AddBusinessRecipientsSelector( not_null container,