From e3f6c189a72f61723d1cc9618597f670a5837391 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 5 Mar 2024 20:52:14 +0400 Subject: [PATCH] Implement preview and save of chatbots. --- Telegram/Resources/langs/lang.strings | 5 +- .../data/business/data_business_chatbots.cpp | 67 +++- .../data/business/data_business_chatbots.h | 16 +- .../data/business/data_business_common.cpp | 124 ++++++++ .../data/business/data_business_common.h | 20 ++ .../data/business/data_business_info.cpp | 51 --- Telegram/SourceFiles/data/data_user.cpp | 96 ------ .../info/profile/info_profile_actions.cpp | 8 +- .../business/settings_away_message.cpp | 4 +- .../settings/business/settings_chatbots.cpp | 297 +++++++++++++++++- .../settings/business/settings_greeting.cpp | 4 +- Telegram/SourceFiles/settings/settings.style | 8 +- .../settings/settings_business.cpp | 12 +- 13 files changed, 536 insertions(+), 176 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 96055ee66..e8366f8f7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2220,7 +2220,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_replies_add_placeholder" = "Shortcut"; "lng_replies_add_exists" = "This shortcut already exists."; "lng_replies_empty_title" = "New Quick Reply"; -"lng_replies_empty_about" = "Enter a message below that will be sent in chat when you type {shortcut}.\n\nYou can access Quick Replies in any chat by typing / or using Attachment menu."; +"lng_replies_empty_about" = "Enter a message below that will be sent in chat when you type {shortcut}.\n\nYou can access Quick Replies in any chat by typing /."; "lng_replies_remove_title" = "Remove Shortcut"; "lng_replies_remove_text" = "You didn't create a quick reply message. Do you want to remove the shortcut?"; "lng_replies_edit_title" = "Edit Shortcut"; @@ -2241,6 +2241,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_greeting_empty_about" = "Create greetings that will be automatically sent to new customers."; "lng_greeting_message_placeholder" = "Add a Greeting"; "lng_greeting_limit_reached" = "You have too many quick replies. Remove one to add a greeting message."; +"lng_greeting_recipients_empty" = "Please choose at least one recipient."; "lng_away_title" = "Away Message"; "lng_away_about" = "Automatically reply with a message when you are away."; @@ -2282,7 +2283,7 @@ 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_not_found" = "Chatbot not found."; "lng_chatbots_add" = "Add"; "lng_boost_channel_button" = "Boost Channel"; diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index 89215a2e0..8862c4e84 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -7,14 +7,46 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/business/data_business_chatbots.h" +#include "apiwrap.h" +#include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" + namespace Data { -Chatbots::Chatbots(not_null session) -: _session(session) { +Chatbots::Chatbots(not_null owner) +: _owner(owner) { } Chatbots::~Chatbots() = default; +void Chatbots::preload() { + if (_loaded || _requestId) { + return; + } + _requestId = _owner->session().api().request( + MTPaccount_GetConnectedBots() + ).done([=](const MTPaccount_ConnectedBots &result) { + _requestId = 0; + _loaded = true; + + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + const auto &list = data.vconnected_bots().v; + if (!list.isEmpty()) { + const auto &bot = list.front().data(); + const auto botId = bot.vbot_id().v; + _settings = ChatbotsSettings{ + .bot = _owner->session().data().user(botId), + .recipients = FromMTP(_owner, bot.vrecipients()), + .repliesAllowed = bot.is_can_reply(), + }; + } + }).send(); +} + const ChatbotsSettings &Chatbots::current() const { return _settings.current(); } @@ -27,7 +59,36 @@ rpl::producer Chatbots::value() const { return _settings.value(); } -void Chatbots::save(ChatbotsSettings settings, Fn fail) { +void Chatbots::save( + ChatbotsSettings settings, + Fn done, + Fn fail) { + const auto was = _settings.current(); + if (was == settings) { + return; + } else if (was.bot || settings.bot) { + using Flag = MTPaccount_UpdateConnectedBot::Flag; + const auto api = &_owner->session().api(); + api->request(MTPaccount_UpdateConnectedBot( + MTP_flags(!settings.bot + ? Flag::f_deleted + : settings.repliesAllowed + ? Flag::f_can_reply + : Flag()), + (settings.bot ? settings.bot : was.bot)->inputUser, + ToMTP(settings.recipients) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + if (done) { + done(); + } + }).fail([=](const MTP::Error &error) { + _settings = was; + if (fail) { + fail(error.type()); + } + }).send(); + } _settings = settings; } diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index da088c394..ca21baef6 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -19,23 +19,33 @@ struct ChatbotsSettings { UserData *bot = nullptr; BusinessRecipients recipients; bool repliesAllowed = false; + + friend inline bool operator==( + const ChatbotsSettings &, + const ChatbotsSettings &) = default; }; class Chatbots final { public: - explicit Chatbots(not_null session); + explicit Chatbots(not_null owner); ~Chatbots(); + void preload(); [[nodiscard]] const ChatbotsSettings ¤t() const; [[nodiscard]] rpl::producer changes() const; [[nodiscard]] rpl::producer value() const; - void save(ChatbotsSettings settings, Fn fail); + void save( + ChatbotsSettings settings, + Fn done, + Fn fail); private: - const not_null _session; + const not_null _owner; rpl::variable _settings; + mtpRequestId _requestId = 0; + bool _loaded = false; }; diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 14b1b0903..7da2970db 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/business/data_business_common.h" +#include "data/data_session.h" +#include "data/data_user.h" + namespace Data { namespace { @@ -50,6 +53,127 @@ constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax; } // namespace +MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data) { + using Flag = MTPDinputBusinessRecipients::Flag; + using Type = BusinessChatType; + const auto &chats = data.allButExcluded + ? data.excluded + : data.included; + const auto flags = Flag() + | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) + | ((chats.types & Type::ExistingChats) + ? Flag::f_existing_chats + : Flag()) + | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) + | ((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; + return MTP_inputBusinessRecipients( + MTP_flags(flags), + MTP_vector_from_range(users.list + | ranges::views::transform(&UserData::inputUser))); +} + +BusinessRecipients FromMTP( + not_null owner, + const MTPBusinessRecipients &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))); + } + } + return result; +} + +[[nodiscard]] BusinessDetails FromMTP( + const tl::conditional &hours, + const tl::conditional &location) { + auto result = BusinessDetails(); + if (hours) { + const auto &data = hours->data(); + result.hours.timezoneId = qs(data.vtimezone_id()); + result.hours.intervals.list = ranges::views::all( + data.vweekly_open().v + ) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) { + const auto &data = open.data(); + return WorkingInterval{ + data.vstart_minute().v * 60, + data.vend_minute().v * 60, + }; + }) | ranges::to_vector; + } + if (location) { + const auto &data = location->data(); + result.location.address = qs(data.vaddress()); + if (const auto point = data.vgeo_point()) { + point->match([&](const MTPDgeoPoint &data) { + result.location.point = LocationPoint(data); + }, [&](const MTPDgeoPointEmpty &) { + }); + } + } + return result; +} + +[[nodiscard]] AwaySettings FromMTP( + not_null owner, + const tl::conditional &message) { + if (!message) { + return AwaySettings(); + } + const auto &data = message->data(); + auto result = AwaySettings{ + .recipients = FromMTP(owner, data.vrecipients()), + .shortcutId = data.vshortcut_id().v, + .offlineOnly = data.is_offline_only(), + }; + data.vschedule().match([&]( + const MTPDbusinessAwayMessageScheduleAlways &) { + result.schedule.type = AwayScheduleType::Always; + }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { + result.schedule.type = AwayScheduleType::OutsideWorkingHours; + }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { + result.schedule.type = AwayScheduleType::Custom; + result.schedule.customInterval = WorkingInterval{ + data.vstart_date().v, + data.vend_date().v, + }; + }); + return result; +} + +[[nodiscard]] GreetingSettings FromMTP( + not_null owner, + const tl::conditional &message) { + if (!message) { + return GreetingSettings(); + } + const auto &data = message->data(); + return GreetingSettings{ + .recipients = FromMTP(owner, data.vrecipients()), + .noActivityDays = data.vno_activity_days().v, + .shortcutId = data.vshortcut_id().v, + }; +} + WorkingIntervals WorkingIntervals::normalized() const { return SortAndMerge(MoveTailToFront(SortAndMerge(*this))); } diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 86422bc98..dd66bb770 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -14,6 +14,8 @@ class UserData; namespace Data { +class Session; + enum class BusinessChatType { NewChats = (1 << 0), ExistingChats = (1 << 1), @@ -43,6 +45,12 @@ struct BusinessRecipients { const BusinessRecipients &b) = default; }; +[[nodiscard]] MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data); +[[nodiscard]] BusinessRecipients FromMTP( + not_null owner, + const MTPBusinessRecipients &recipients); + struct Timezone { QString id; QString name; @@ -173,6 +181,10 @@ struct BusinessDetails { const BusinessDetails &b) = default; }; +[[nodiscard]] BusinessDetails FromMTP( + const tl::conditional &hours, + const tl::conditional &location); + enum class AwayScheduleType : uchar { Never = 0, Always = 1, @@ -204,6 +216,10 @@ struct AwaySettings { const AwaySettings &b) = default; }; +[[nodiscard]] AwaySettings FromMTP( + not_null owner, + const tl::conditional &message); + struct GreetingSettings { BusinessRecipients recipients; int noActivityDays = 0; @@ -218,4 +234,8 @@ struct GreetingSettings { const GreetingSettings &b) = default; }; +[[nodiscard]] GreetingSettings FromMTP( + not_null owner, + const tl::conditional &message); + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index d874e52b9..b6cf5ac36 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -30,57 +30,6 @@ namespace { MTP_vector_from_range(list | ranges::views::transform(proj))); } -[[nodiscard]] MTPInputBusinessRecipients ToMTP( - const BusinessRecipients &data) { - //MTP_flags(RecipientsFlags(data.recipients, Flag())), - // MTP_vector_from_range( - // (data.recipients.allButExcluded - // ? data.recipients.excluded - // : data.recipients.included).list - // | ranges::views::transform(&UserData::inputUser)), - - using Flag = MTPDinputBusinessRecipients::Flag; - using Type = BusinessChatType; - const auto &chats = data.allButExcluded - ? data.excluded - : data.included; - const auto flags = Flag() - | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) - | ((chats.types & Type::ExistingChats) - ? Flag::f_existing_chats - : Flag()) - | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) - | ((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; - return MTP_inputBusinessRecipients( - MTP_flags(flags), - MTP_vector_from_range(users.list - | ranges::views::transform(&UserData::inputUser))); -} - -template -[[nodiscard]] auto RecipientsFlags( - const BusinessRecipients &data, - Flag) { - using Type = BusinessChatType; - const auto &chats = data.allButExcluded - ? data.excluded - : data.included; - return Flag() - | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) - | ((chats.types & Type::ExistingChats) - ? Flag::f_existing_chats - : Flag()) - | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) - | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) - | (chats.list.empty() ? Flag() : Flag::f_users) - | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); -} - [[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP( const AwaySchedule &data) { Expects(data.type != AwayScheduleType::Never); diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index d670d9308..a5c2a705d 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -32,102 +32,6 @@ constexpr auto kSetOnlineAfterActivity = TimeId(30); using UpdateFlag = Data::PeerUpdate::Flag; -[[nodiscard]] Data::BusinessDetails FromMTP( - const tl::conditional &hours, - const tl::conditional &location) { - auto result = Data::BusinessDetails(); - if (hours) { - const auto &data = hours->data(); - result.hours.timezoneId = qs(data.vtimezone_id()); - result.hours.intervals.list = ranges::views::all( - data.vweekly_open().v - ) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) { - const auto &data = open.data(); - return Data::WorkingInterval{ - data.vstart_minute().v * 60, - data.vend_minute().v * 60, - }; - }) | ranges::to_vector; - } - if (location) { - const auto &data = location->data(); - result.location.address = qs(data.vaddress()); - if (const auto point = data.vgeo_point()) { - point->match([&](const MTPDgeoPoint &data) { - result.location.point = Data::LocationPoint(data); - }, [&](const MTPDgeoPointEmpty &) { - }); - } - } - return result; -} - -Data::BusinessRecipients FromMTP( - not_null owner, - const MTPBusinessRecipients &recipients) { - using Type = Data::BusinessChatType; - - const auto &data = recipients.data(); - auto result = Data::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))); - } - } - return result; -} - -[[nodiscard]] Data::AwaySettings FromMTP( - not_null owner, - const tl::conditional &message) { - if (!message) { - return Data::AwaySettings(); - } - const auto &data = message->data(); - auto result = Data::AwaySettings{ - .recipients = FromMTP(owner, data.vrecipients()), - .shortcutId = data.vshortcut_id().v, - .offlineOnly = data.is_offline_only(), - }; - data.vschedule().match([&]( - const MTPDbusinessAwayMessageScheduleAlways &) { - result.schedule.type = Data::AwayScheduleType::Always; - }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { - result.schedule.type = Data::AwayScheduleType::OutsideWorkingHours; - }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { - result.schedule.type = Data::AwayScheduleType::Custom; - result.schedule.customInterval = Data::WorkingInterval{ - data.vstart_date().v, - data.vend_date().v, - }; - }); - return result; -} - -[[nodiscard]] Data::GreetingSettings FromMTP( - not_null owner, - const tl::conditional &message) { - if (!message) { - return Data::GreetingSettings(); - } - const auto &data = message->data(); - return Data::GreetingSettings{ - .recipients = FromMTP(owner, data.vrecipients()), - .noActivityDays = data.vno_activity_days().v, - .shortcutId = data.vshortcut_id().v, - }; -} - } // namespace BotInfo::BotInfo() = default; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index f0d690b51..ddcb93e45 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -211,12 +211,14 @@ base::options::toggle ShowPeerIdBelowAbout({ [[nodiscard]] rpl::producer OpensInText( rpl::producer in, + rpl::producer hoursExpanded, rpl::producer fallback) { return rpl::combine( std::move(in), + std::move(hoursExpanded), std::move(fallback) - ) | rpl::map([](TimeId in, QString fallback) { - return !in + ) | rpl::map([](TimeId in, bool hoursExpanded, QString fallback) { + return (!in || hoursExpanded) ? std::move(fallback) : (in >= 86400) ? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400) @@ -465,6 +467,7 @@ base::options::toggle ShowPeerIdBelowAbout({ openedWrap, OpensInText( state->opensIn.value(), + state->expanded.value(), dayHoursTextValue(state->day.value()) ) | rpl::after_next(recount), st::infoHoursValue); @@ -518,6 +521,7 @@ base::options::toggle ShowPeerIdBelowAbout({ }, link->lifetime()); link->setClickedCallback([=] { state->myTimezone = !state->myTimezone.current(); + state->expanded = true; }); rpl::combine( diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 04dfe993a..a31e2958f 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -346,9 +346,7 @@ void AwayMessage::save() { const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { - AssertIsDebug(); - show->showToast(u"Please choose at least one recipient."_q); - //tr::lng_greeting_recipients_empty(tr::now)); + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); } else if (error != u"SHORTCUT_INVALID"_q) { show->showToast(error); } diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index d3f112483..d9b17f260 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" +#include "apiwrap.h" +#include "boxes/peers/prepare_short_info_box.h" +#include "boxes/peer_list_box.h" #include "core/application.h" #include "data/business/data_business_chatbots.h" #include "data/data_session.h" @@ -14,19 +17,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { namespace { +constexpr auto kDebounceTimeout = crl::time(400); + enum class LookupState { Empty, Loading, @@ -64,22 +72,298 @@ private: }; +class PreviewController final : public PeerListController { +public: + PreviewController(not_null peer, Fn resetBot); + + void prepare() override; + void loadMoreRows() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + Main::Session &session() const override; + +private: + const not_null _peer; + const Fn _resetBot; + rpl::lifetime _lifetime; + +}; + +class PreviewRow final : public PeerListRow { +public: + using PeerListRow::PeerListRow; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + void rightActionAddRipple( + QPoint point, + Fn updateCallback) override; + void rightActionStopLastRipple() override; + +private: + std::unique_ptr _actionRipple; + +}; + +QSize PreviewRow::rightActionSize() const { + return QSize( + st::settingsChatbotsDeleteIcon.width(), + st::settingsChatbotsDeleteIcon.height()) * 2; +} + +QMargins PreviewRow::rightActionMargins() const { + const auto itemHeight = st::peerListSingleRow.item.height; + const auto skip = (itemHeight - rightActionSize().height()) / 2; + return QMargins(0, skip, skip, 0); +} + +void PreviewRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (_actionRipple) { + _actionRipple->paint( + p, + x, + y, + outerWidth); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + const auto rect = QRect(QPoint(x, y), PreviewRow::rightActionSize()); + (actionSelected + ? st::settingsChatbotsDeleteIconOver + : st::settingsChatbotsDeleteIcon).paintInCenter(p, rect); +} + +void PreviewRow::rightActionAddRipple( + QPoint point, + Fn updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize()); + _actionRipple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point); +} + +void PreviewRow::rightActionStopLastRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + +PreviewController::PreviewController( + not_null peer, + Fn resetBot) +: _peer(peer) +, _resetBot(std::move(resetBot)) { +} + +void PreviewController::prepare() { + delegate()->peerListAppendRow(std::make_unique(_peer)); + delegate()->peerListRefreshRows(); +} + +void PreviewController::loadMoreRows() { +} + +void PreviewController::rowClicked(not_null row) { +} + +void PreviewController::rowRightActionClicked(not_null row) { + _resetBot(); +} + +Main::Session &PreviewController::session() const { + return _peer->session(); +} + [[nodiscard]] rpl::producer DebouncedValue( not_null field) { - return rpl::single(field->getLastText()); + return [=](auto consumer) { + + auto result = rpl::lifetime(); + struct State { + base::Timer timer; + QString lastText; + }; + const auto state = result.make_state(); + const auto push = [=] { + state->timer.cancel(); + consumer.put_next_copy(state->lastText); + }; + state->timer.setCallback(push); + state->lastText = field->getLastText(); + consumer.put_next_copy(field->getLastText()); + field->changes() | rpl::start_with_next([=] { + const auto &text = field->getLastText(); + const auto was = std::exchange(state->lastText, text); + if (std::abs(int(text.size()) - int(was.size())) == 1) { + state->timer.callOnce(kDebounceTimeout); + } else { + push(); + } + }, result); + return result; + }; +} + +[[nodiscard]] QString ExtractUsername(QString text) { + text = text.trimmed(); + static const auto expression = QRegularExpression( + "^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)"); + const auto match = expression.match(text); + return match.hasMatch() ? match.captured(3) : text; } [[nodiscard]] rpl::producer LookupBot( not_null session, rpl::producer usernameChanges) { - return rpl::never(); + using Cache = base::flat_map; + const auto cache = std::make_shared(); + return std::move( + usernameChanges + ) | rpl::map([=](const QString &username) -> rpl::producer { + const auto extracted = ExtractUsername(username); + const auto owner = &session->data(); + static const auto expression = QRegularExpression( + "^[a-zA-Z0-9_\\.]+$"); + if (!expression.match(extracted).hasMatch()) { + return rpl::single(BotState()); + } else if (const auto peer = owner->peerByUsername(extracted)) { + if (const auto user = peer->asUser(); user && user->isBot()) { + return rpl::single(BotState{ + .bot = user, + .state = LookupState::Ready, + }); + } + return rpl::single(BotState{ + .state = LookupState::Ready, + }); + } else if (const auto i = cache->find(extracted); i != end(*cache)) { + return rpl::single(BotState{ + .bot = i->second, + .state = LookupState::Ready, + }); + } + + return [=](auto consumer) { + auto result = rpl::lifetime(); + + const auto requestId = result.make_state(); + *requestId = session->api().request(MTPcontacts_ResolveUsername( + MTP_string(extracted) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + const auto &data = result.data(); + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + const auto peerId = peerFromMTP(data.vpeer()); + const auto peer = session->data().peer(peerId); + if (const auto user = peer->asUser()) { + if (user->isBot()) { + cache->emplace(extracted, user); + consumer.put_next(BotState{ + .bot = user, + .state = LookupState::Ready, + }); + return; + } + } + cache->emplace(extracted, nullptr); + consumer.put_next(BotState{ .state = LookupState::Ready }); + }).fail([=] { + cache->emplace(extracted, nullptr); + consumer.put_next(BotState{ .state = LookupState::Ready }); + }).send(); + + result.add([=] { + session->api().request(*requestId).cancel(); + }); + return result; + }; + }) | rpl::flatten_latest(); } [[nodiscard]] object_ptr MakeBotPreview( - not_null parent, + not_null parent, rpl::producer state, Fn resetBot) { - return object_ptr(parent.get()); + auto result = object_ptr>( + parent.get(), + object_ptr(parent.get())); + const auto raw = result.data(); + const auto inner = raw->entity(); + raw->hide(anim::type::instant); + + const auto child = inner->lifetime().make_state(nullptr); + std::move(state) | rpl::filter([=](BotState state) { + return state.state != LookupState::Loading; + }) | rpl::start_with_next([=](BotState state) { + raw->toggle(state.state == LookupState::Ready, anim::type::normal); + if (state.bot) { + const auto delegate = parent->lifetime().make_state< + PeerListContentDelegateSimple + >(); + const auto controller = parent->lifetime().make_state< + PreviewController + >(state.bot, resetBot); + controller->setStyleOverrides(&st::peerListSingleRow); + const auto content = Ui::CreateChild( + inner, + controller); + delegate->setContent(content); + controller->setDelegate(delegate); + delete base::take(*child); + *child = content; + } else if (state.state == LookupState::Ready) { + const auto content = Ui::CreateChild(inner); + const auto label = Ui::CreateChild( + content, + tr::lng_chatbots_not_found(), + st::settingsChatbotsNotFound); + content->resize( + inner->width(), + st::peerListSingleRow.item.height); + rpl::combine( + content->sizeValue(), + label->sizeValue() + ) | rpl::start_with_next([=](QSize size, QSize inner) { + label->move( + (size.width() - inner.width()) / 2, + (size.height() - inner.height()) / 2); + }, label->lifetime()); + delete base::take(*child); + *child = content; + } else { + return; + } + (*child)->show(); + + inner->widthValue() | rpl::start_with_next([=](int width) { + (*child)->resizeToWidth(width); + }, (*child)->lifetime()); + + (*child)->heightValue() | rpl::start_with_next([=](int height) { + inner->resize(inner->width(), height + st::contactSkip); + }, inner->lifetime()); + }, inner->lifetime()); + + raw->finishAnimating(); + return result; } Chatbots::Chatbots( @@ -193,15 +477,14 @@ void Chatbots::save() { const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { - AssertIsDebug(); - show->showToast(u"Please choose at least one recipient."_q); - //tr::lng_greeting_recipients_empty(tr::now)); + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); } }; controller()->session().data().chatbots().save({ .bot = _botValue.current().bot, .recipients = _recipients.current(), .repliesAllowed = _repliesAllowed.current(), + }, [=] { }, [=](QString error) { show->showToast(error); }); } diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 3c73f391f..932c1d980 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -263,9 +263,7 @@ void Greeting::save() { const auto session = &controller()->session(); const auto fail = [=](QString error) { if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { - AssertIsDebug(); - show->showToast(u"Please choose at least one recipient."_q); - //tr::lng_greeting_recipients_empty(tr::now)); + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); } else if (error != u"SHORTCUT_INVALID"_q) { show->showToast(error); } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index fcdd0a7cd..00f1121d1 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -631,4 +631,10 @@ settingsAddReplyField: InputField(defaultInputField) { placeholderScale: 0.; heightMin: 36px; -} \ No newline at end of file +} +settingsChatbotsNotFound: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + align: align(top); +} +settingsChatbotsDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; +settingsChatbotsDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }}; diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index c0848e341..936f0c49f 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -9,10 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "core/click_handler_types.h" +#include "data/business/data_business_info.h" +#include "data/business/data_business_chatbots.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_peer_values.h" // AmPremiumValue. #include "data/data_session.h" -#include "data/business/data_business_info.h" -#include "data/business/data_shortcut_messages.h" #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" @@ -224,9 +225,9 @@ void AddBusinessSummary( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = account.appConfig().get( + const auto mtpOrder = FallbackOrder(); AssertIsDebug();/* account.appConfig().get( "business_promo_order", - FallbackOrder()); + FallbackOrder());*/ const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); @@ -354,7 +355,8 @@ void Business::setStepDataReference(std::any &data) { void Business::setupContent() { const auto content = Ui::CreateChild(this); - _controller->session().data().businessInfo().preloadTimezones(); + _controller->session().data().chatbots().preload(); + _controller->session().data().businessInfo().preload(); _controller->session().data().shortcutMessages().preloadShortcuts(); Ui::AddSkip(content, st::settingsFromFileTop);