| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- /*
- 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_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"
- #include "data/data_user.h"
- #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,
- Unsupported,
- Ready,
- };
- struct BotState {
- UserData *bot = nullptr;
- LookupState state = LookupState::Empty;
- };
- class Chatbots final : public BusinessSection<Chatbots> {
- public:
- Chatbots(
- QWidget *parent,
- not_null<Window::SessionController*> controller);
- ~Chatbots();
- [[nodiscard]] bool closeByOutsideClick() const override;
- [[nodiscard]] rpl::producer<QString> title() override;
- const Ui::RoundRect *bottomSkipRounding() const override {
- return &_bottomSkipRounding;
- }
- private:
- void setupContent(not_null<Window::SessionController*> controller);
- void save();
- Ui::RoundRect _bottomSkipRounding;
- rpl::variable<Data::BusinessRecipients> _recipients;
- rpl::variable<QString> _usernameValue;
- rpl::variable<BotState> _botValue;
- rpl::variable<bool> _repliesAllowed = true;
- };
- class PreviewController final : public PeerListController {
- public:
- PreviewController(not_null<PeerData*> peer, Fn<void()> resetBot);
- void prepare() override;
- void loadMoreRows() override;
- void rowClicked(not_null<PeerListRow*> row) override;
- void rowRightActionClicked(not_null<PeerListRow*> row) override;
- Main::Session &session() const override;
- private:
- const not_null<PeerData*> _peer;
- const Fn<void()> _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<void()> updateCallback) override;
- void rightActionStopLastRipple() override;
- private:
- std::unique_ptr<Ui::RippleAnimation> _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<void()> updateCallback) {
- if (!_actionRipple) {
- auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());
- _actionRipple = std::make_unique<Ui::RippleAnimation>(
- st::defaultRippleAnimation,
- std::move(mask),
- std::move(updateCallback));
- }
- _actionRipple->add(point);
- }
- void PreviewRow::rightActionStopLastRipple() {
- if (_actionRipple) {
- _actionRipple->lastStop();
- }
- }
- PreviewController::PreviewController(
- not_null<PeerData*> peer,
- Fn<void()> resetBot)
- : _peer(peer)
- , _resetBot(std::move(resetBot)) {
- }
- void PreviewController::prepare() {
- delegate()->peerListAppendRow(std::make_unique<PreviewRow>(_peer));
- delegate()->peerListRefreshRows();
- }
- void PreviewController::loadMoreRows() {
- }
- void PreviewController::rowClicked(not_null<PeerListRow*> row) {
- }
- void PreviewController::rowRightActionClicked(not_null<PeerListRow*> row) {
- _resetBot();
- }
- Main::Session &PreviewController::session() const {
- return _peer->session();
- }
- [[nodiscard]] rpl::producer<QString> DebouncedValue(
- not_null<Ui::InputField*> field) {
- return [=](auto consumer) {
- auto result = rpl::lifetime();
- struct State {
- base::Timer timer;
- QString lastText;
- };
- const auto state = result.make_state<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();
- if (text.startsWith(QChar('@'))) {
- return text.mid(1);
- }
- 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<BotState> LookupBot(
- not_null<Main::Session*> session,
- rpl::producer<QString> usernameChanges) {
- using Cache = base::flat_map<QString, UserData*>;
- const auto cache = std::make_shared<Cache>();
- return std::move(
- usernameChanges
- ) | rpl::map([=](const QString &username) -> rpl::producer<BotState> {
- 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()) {
- if (user->botInfo->supportsBusiness) {
- return rpl::single(BotState{
- .bot = user,
- .state = LookupState::Ready,
- });
- }
- return rpl::single(BotState{
- .state = LookupState::Unsupported,
- });
- }
- 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<mtpRequestId>();
- *requestId = session->api().request(MTPcontacts_ResolveUsername(
- MTP_flags(0),
- MTP_string(extracted),
- MTP_string()
- )).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<Ui::RpWidget> MakeBotPreview(
- not_null<Ui::RpWidget*> parent,
- rpl::producer<BotState> state,
- Fn<void()> resetBot) {
- auto result = object_ptr<Ui::SlideWrap<>>(
- parent.get(),
- object_ptr<Ui::RpWidget>(parent.get()));
- const auto raw = result.data();
- const auto inner = raw->entity();
- raw->hide(anim::type::instant);
- const auto child = inner->lifetime().make_state<Ui::RpWidget*>(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
- || state.state == LookupState::Unsupported),
- 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<PeerListContent>(
- inner,
- controller);
- delegate->setContent(content);
- controller->setDelegate(delegate);
- delete base::take(*child);
- *child = content;
- } else if (state.state == LookupState::Ready
- || state.state == LookupState::Unsupported) {
- const auto content = Ui::CreateChild<Ui::RpWidget>(inner);
- const auto label = Ui::CreateChild<Ui::FlatLabel>(
- content,
- (state.state == LookupState::Unsupported
- ? tr::lng_chatbots_not_supported()
- : 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(
- QWidget *parent,
- not_null<Window::SessionController*> controller)
- : BusinessSection(parent, controller)
- , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
- setupContent(controller);
- }
- Chatbots::~Chatbots() {
- if (!Core::Quitting()) {
- save();
- }
- }
- bool Chatbots::closeByOutsideClick() const {
- return false;
- }
- rpl::producer<QString> Chatbots::title() {
- return tr::lng_chatbots_title();
- }
- void Chatbots::setupContent(
- not_null<Window::SessionController*> controller) {
- using namespace rpl::mappers;
- const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
- const auto current = controller->session().data().chatbots().current();
- _recipients = Data::BusinessRecipients::MakeValid(current.recipients);
- _repliesAllowed = current.repliesAllowed;
- AddDividerTextWithLottie(content, {
- .lottie = u"robot"_q,
- .lottieSize = st::settingsCloudPasswordIconSize,
- .lottieMargins = st::peerAppearanceIconPadding,
- .showFinished = showFinishes(),
- .about = tr::lng_chatbots_about(
- lt_link,
- tr::lng_chatbots_about_link(
- ) | Ui::Text::ToLink(tr::lng_chatbots_info_url(tr::now)),
- Ui::Text::WithEntities),
- .aboutMargins = st::peerAppearanceCoverLabelMargin,
- });
- const auto username = content->add(
- object_ptr<Ui::InputField>(
- content,
- st::settingsChatbotsUsername,
- tr::lng_chatbots_placeholder(),
- (current.bot
- ? current.bot->session().createInternalLink(
- current.bot->username())
- : QString())),
- st::settingsChatbotsUsernameMargins);
- _usernameValue = DebouncedValue(username);
- _botValue = rpl::single(BotState{
- current.bot,
- current.bot ? LookupState::Ready : LookupState::Empty
- }) | rpl::then(
- LookupBot(&controller->session(), _usernameValue.changes())
- );
- const auto resetBot = [=] {
- username->setText(QString());
- username->setFocus();
- };
- content->add(object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
- content,
- MakeBotPreview(content, _botValue.value(), resetBot)));
- Ui::AddDividerText(
- content,
- tr::lng_chatbots_add_about(),
- st::peerAppearanceDividerTextMargin);
- AddBusinessRecipientsSelector(content, {
- .controller = controller,
- .title = tr::lng_chatbots_access_title(),
- .data = &_recipients,
- .type = Data::BusinessRecipientsType::Bots,
- });
- Ui::AddSkip(content, st::settingsChatbotsAccessSkip);
- Ui::AddDividerText(
- content,
- tr::lng_chatbots_exclude_about(),
- st::peerAppearanceDividerTextMargin);
- Ui::AddSkip(content);
- Ui::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title());
- content->add(object_ptr<Ui::SettingsButton>(
- content,
- tr::lng_chatbots_reply(),
- st::settingsButtonNoIcon
- ))->toggleOn(_repliesAllowed.value())->toggledChanges(
- ) | rpl::start_with_next([=](bool value) {
- _repliesAllowed = value;
- }, content->lifetime());
- Ui::AddSkip(content);
- Ui::AddDividerText(
- content,
- tr::lng_chatbots_reply_about(),
- st::settingsChatbotsBottomTextMargin,
- RectPart::Top);
- Ui::ResizeFitChild(this, content);
- }
- void Chatbots::save() {
- const auto show = controller()->uiShow();
- const auto fail = [=](QString error) {
- if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) {
- show->showToast(tr::lng_greeting_recipients_empty(tr::now));
- } else if (error == u"BOT_BUSINESS_MISSING"_q) {
- show->showToast(tr::lng_chatbots_not_supported(tr::now));
- }
- };
- controller()->session().data().chatbots().save({
- .bot = _botValue.current().bot,
- .recipients = _recipients.current(),
- .repliesAllowed = _repliesAllowed.current(),
- }, [=] {
- }, fail);
- }
- } // namespace
- Type ChatbotsId() {
- return Chatbots::Id();
- }
- } // namespace Settings
|