settings_chatbots.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "settings/business/settings_chatbots.h"
  8. #include "apiwrap.h"
  9. #include "boxes/peers/prepare_short_info_box.h"
  10. #include "boxes/peer_list_box.h"
  11. #include "core/application.h"
  12. #include "data/business/data_business_chatbots.h"
  13. #include "data/data_session.h"
  14. #include "data/data_user.h"
  15. #include "lang/lang_keys.h"
  16. #include "main/main_session.h"
  17. #include "settings/business/settings_recipients_helper.h"
  18. #include "ui/effects/ripple_animation.h"
  19. #include "ui/text/text_utilities.h"
  20. #include "ui/widgets/fields/input_field.h"
  21. #include "ui/widgets/buttons.h"
  22. #include "ui/wrap/slide_wrap.h"
  23. #include "ui/wrap/vertical_layout.h"
  24. #include "ui/painter.h"
  25. #include "ui/vertical_list.h"
  26. #include "window/window_session_controller.h"
  27. #include "styles/style_boxes.h"
  28. #include "styles/style_layers.h"
  29. #include "styles/style_settings.h"
  30. namespace Settings {
  31. namespace {
  32. constexpr auto kDebounceTimeout = crl::time(400);
  33. enum class LookupState {
  34. Empty,
  35. Loading,
  36. Unsupported,
  37. Ready,
  38. };
  39. struct BotState {
  40. UserData *bot = nullptr;
  41. LookupState state = LookupState::Empty;
  42. };
  43. class Chatbots final : public BusinessSection<Chatbots> {
  44. public:
  45. Chatbots(
  46. QWidget *parent,
  47. not_null<Window::SessionController*> controller);
  48. ~Chatbots();
  49. [[nodiscard]] bool closeByOutsideClick() const override;
  50. [[nodiscard]] rpl::producer<QString> title() override;
  51. const Ui::RoundRect *bottomSkipRounding() const override {
  52. return &_bottomSkipRounding;
  53. }
  54. private:
  55. void setupContent(not_null<Window::SessionController*> controller);
  56. void save();
  57. Ui::RoundRect _bottomSkipRounding;
  58. rpl::variable<Data::BusinessRecipients> _recipients;
  59. rpl::variable<QString> _usernameValue;
  60. rpl::variable<BotState> _botValue;
  61. rpl::variable<bool> _repliesAllowed = true;
  62. };
  63. class PreviewController final : public PeerListController {
  64. public:
  65. PreviewController(not_null<PeerData*> peer, Fn<void()> resetBot);
  66. void prepare() override;
  67. void loadMoreRows() override;
  68. void rowClicked(not_null<PeerListRow*> row) override;
  69. void rowRightActionClicked(not_null<PeerListRow*> row) override;
  70. Main::Session &session() const override;
  71. private:
  72. const not_null<PeerData*> _peer;
  73. const Fn<void()> _resetBot;
  74. rpl::lifetime _lifetime;
  75. };
  76. class PreviewRow final : public PeerListRow {
  77. public:
  78. using PeerListRow::PeerListRow;
  79. QSize rightActionSize() const override;
  80. QMargins rightActionMargins() const override;
  81. void rightActionPaint(
  82. Painter &p,
  83. int x,
  84. int y,
  85. int outerWidth,
  86. bool selected,
  87. bool actionSelected) override;
  88. void rightActionAddRipple(
  89. QPoint point,
  90. Fn<void()> updateCallback) override;
  91. void rightActionStopLastRipple() override;
  92. private:
  93. std::unique_ptr<Ui::RippleAnimation> _actionRipple;
  94. };
  95. QSize PreviewRow::rightActionSize() const {
  96. return QSize(
  97. st::settingsChatbotsDeleteIcon.width(),
  98. st::settingsChatbotsDeleteIcon.height()) * 2;
  99. }
  100. QMargins PreviewRow::rightActionMargins() const {
  101. const auto itemHeight = st::peerListSingleRow.item.height;
  102. const auto skip = (itemHeight - rightActionSize().height()) / 2;
  103. return QMargins(0, skip, skip, 0);
  104. }
  105. void PreviewRow::rightActionPaint(
  106. Painter &p,
  107. int x,
  108. int y,
  109. int outerWidth,
  110. bool selected,
  111. bool actionSelected) {
  112. if (_actionRipple) {
  113. _actionRipple->paint(p, x, y, outerWidth);
  114. if (_actionRipple->empty()) {
  115. _actionRipple.reset();
  116. }
  117. }
  118. const auto rect = QRect(QPoint(x, y), PreviewRow::rightActionSize());
  119. (actionSelected
  120. ? st::settingsChatbotsDeleteIconOver
  121. : st::settingsChatbotsDeleteIcon).paintInCenter(p, rect);
  122. }
  123. void PreviewRow::rightActionAddRipple(
  124. QPoint point,
  125. Fn<void()> updateCallback) {
  126. if (!_actionRipple) {
  127. auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());
  128. _actionRipple = std::make_unique<Ui::RippleAnimation>(
  129. st::defaultRippleAnimation,
  130. std::move(mask),
  131. std::move(updateCallback));
  132. }
  133. _actionRipple->add(point);
  134. }
  135. void PreviewRow::rightActionStopLastRipple() {
  136. if (_actionRipple) {
  137. _actionRipple->lastStop();
  138. }
  139. }
  140. PreviewController::PreviewController(
  141. not_null<PeerData*> peer,
  142. Fn<void()> resetBot)
  143. : _peer(peer)
  144. , _resetBot(std::move(resetBot)) {
  145. }
  146. void PreviewController::prepare() {
  147. delegate()->peerListAppendRow(std::make_unique<PreviewRow>(_peer));
  148. delegate()->peerListRefreshRows();
  149. }
  150. void PreviewController::loadMoreRows() {
  151. }
  152. void PreviewController::rowClicked(not_null<PeerListRow*> row) {
  153. }
  154. void PreviewController::rowRightActionClicked(not_null<PeerListRow*> row) {
  155. _resetBot();
  156. }
  157. Main::Session &PreviewController::session() const {
  158. return _peer->session();
  159. }
  160. [[nodiscard]] rpl::producer<QString> DebouncedValue(
  161. not_null<Ui::InputField*> field) {
  162. return [=](auto consumer) {
  163. auto result = rpl::lifetime();
  164. struct State {
  165. base::Timer timer;
  166. QString lastText;
  167. };
  168. const auto state = result.make_state<State>();
  169. const auto push = [=] {
  170. state->timer.cancel();
  171. consumer.put_next_copy(state->lastText);
  172. };
  173. state->timer.setCallback(push);
  174. state->lastText = field->getLastText();
  175. consumer.put_next_copy(field->getLastText());
  176. field->changes() | rpl::start_with_next([=] {
  177. const auto &text = field->getLastText();
  178. const auto was = std::exchange(state->lastText, text);
  179. if (std::abs(int(text.size()) - int(was.size())) == 1) {
  180. state->timer.callOnce(kDebounceTimeout);
  181. } else {
  182. push();
  183. }
  184. }, result);
  185. return result;
  186. };
  187. }
  188. [[nodiscard]] QString ExtractUsername(QString text) {
  189. text = text.trimmed();
  190. if (text.startsWith(QChar('@'))) {
  191. return text.mid(1);
  192. }
  193. static const auto expression = QRegularExpression(
  194. "^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)");
  195. const auto match = expression.match(text);
  196. return match.hasMatch() ? match.captured(3) : text;
  197. }
  198. [[nodiscard]] rpl::producer<BotState> LookupBot(
  199. not_null<Main::Session*> session,
  200. rpl::producer<QString> usernameChanges) {
  201. using Cache = base::flat_map<QString, UserData*>;
  202. const auto cache = std::make_shared<Cache>();
  203. return std::move(
  204. usernameChanges
  205. ) | rpl::map([=](const QString &username) -> rpl::producer<BotState> {
  206. const auto extracted = ExtractUsername(username);
  207. const auto owner = &session->data();
  208. static const auto expression = QRegularExpression(
  209. "^[a-zA-Z0-9_\\.]+$");
  210. if (!expression.match(extracted).hasMatch()) {
  211. return rpl::single(BotState());
  212. } else if (const auto peer = owner->peerByUsername(extracted)) {
  213. if (const auto user = peer->asUser(); user && user->isBot()) {
  214. if (user->botInfo->supportsBusiness) {
  215. return rpl::single(BotState{
  216. .bot = user,
  217. .state = LookupState::Ready,
  218. });
  219. }
  220. return rpl::single(BotState{
  221. .state = LookupState::Unsupported,
  222. });
  223. }
  224. return rpl::single(BotState{
  225. .state = LookupState::Ready,
  226. });
  227. } else if (const auto i = cache->find(extracted); i != end(*cache)) {
  228. return rpl::single(BotState{
  229. .bot = i->second,
  230. .state = LookupState::Ready,
  231. });
  232. }
  233. return [=](auto consumer) {
  234. auto result = rpl::lifetime();
  235. const auto requestId = result.make_state<mtpRequestId>();
  236. *requestId = session->api().request(MTPcontacts_ResolveUsername(
  237. MTP_flags(0),
  238. MTP_string(extracted),
  239. MTP_string()
  240. )).done([=](const MTPcontacts_ResolvedPeer &result) {
  241. const auto &data = result.data();
  242. session->data().processUsers(data.vusers());
  243. session->data().processChats(data.vchats());
  244. const auto peerId = peerFromMTP(data.vpeer());
  245. const auto peer = session->data().peer(peerId);
  246. if (const auto user = peer->asUser()) {
  247. if (user->isBot()) {
  248. cache->emplace(extracted, user);
  249. consumer.put_next(BotState{
  250. .bot = user,
  251. .state = LookupState::Ready,
  252. });
  253. return;
  254. }
  255. }
  256. cache->emplace(extracted, nullptr);
  257. consumer.put_next(BotState{ .state = LookupState::Ready });
  258. }).fail([=] {
  259. cache->emplace(extracted, nullptr);
  260. consumer.put_next(BotState{ .state = LookupState::Ready });
  261. }).send();
  262. result.add([=] {
  263. session->api().request(*requestId).cancel();
  264. });
  265. return result;
  266. };
  267. }) | rpl::flatten_latest();
  268. }
  269. [[nodiscard]] object_ptr<Ui::RpWidget> MakeBotPreview(
  270. not_null<Ui::RpWidget*> parent,
  271. rpl::producer<BotState> state,
  272. Fn<void()> resetBot) {
  273. auto result = object_ptr<Ui::SlideWrap<>>(
  274. parent.get(),
  275. object_ptr<Ui::RpWidget>(parent.get()));
  276. const auto raw = result.data();
  277. const auto inner = raw->entity();
  278. raw->hide(anim::type::instant);
  279. const auto child = inner->lifetime().make_state<Ui::RpWidget*>(nullptr);
  280. std::move(state) | rpl::filter([=](BotState state) {
  281. return state.state != LookupState::Loading;
  282. }) | rpl::start_with_next([=](BotState state) {
  283. raw->toggle(
  284. (state.state == LookupState::Ready
  285. || state.state == LookupState::Unsupported),
  286. anim::type::normal);
  287. if (state.bot) {
  288. const auto delegate = parent->lifetime().make_state<
  289. PeerListContentDelegateSimple
  290. >();
  291. const auto controller = parent->lifetime().make_state<
  292. PreviewController
  293. >(state.bot, resetBot);
  294. controller->setStyleOverrides(&st::peerListSingleRow);
  295. const auto content = Ui::CreateChild<PeerListContent>(
  296. inner,
  297. controller);
  298. delegate->setContent(content);
  299. controller->setDelegate(delegate);
  300. delete base::take(*child);
  301. *child = content;
  302. } else if (state.state == LookupState::Ready
  303. || state.state == LookupState::Unsupported) {
  304. const auto content = Ui::CreateChild<Ui::RpWidget>(inner);
  305. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  306. content,
  307. (state.state == LookupState::Unsupported
  308. ? tr::lng_chatbots_not_supported()
  309. : tr::lng_chatbots_not_found()),
  310. st::settingsChatbotsNotFound);
  311. content->resize(
  312. inner->width(),
  313. st::peerListSingleRow.item.height);
  314. rpl::combine(
  315. content->sizeValue(),
  316. label->sizeValue()
  317. ) | rpl::start_with_next([=](QSize size, QSize inner) {
  318. label->move(
  319. (size.width() - inner.width()) / 2,
  320. (size.height() - inner.height()) / 2);
  321. }, label->lifetime());
  322. delete base::take(*child);
  323. *child = content;
  324. } else {
  325. return;
  326. }
  327. (*child)->show();
  328. inner->widthValue() | rpl::start_with_next([=](int width) {
  329. (*child)->resizeToWidth(width);
  330. }, (*child)->lifetime());
  331. (*child)->heightValue() | rpl::start_with_next([=](int height) {
  332. inner->resize(inner->width(), height + st::contactSkip);
  333. }, inner->lifetime());
  334. }, inner->lifetime());
  335. raw->finishAnimating();
  336. return result;
  337. }
  338. Chatbots::Chatbots(
  339. QWidget *parent,
  340. not_null<Window::SessionController*> controller)
  341. : BusinessSection(parent, controller)
  342. , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
  343. setupContent(controller);
  344. }
  345. Chatbots::~Chatbots() {
  346. if (!Core::Quitting()) {
  347. save();
  348. }
  349. }
  350. bool Chatbots::closeByOutsideClick() const {
  351. return false;
  352. }
  353. rpl::producer<QString> Chatbots::title() {
  354. return tr::lng_chatbots_title();
  355. }
  356. void Chatbots::setupContent(
  357. not_null<Window::SessionController*> controller) {
  358. using namespace rpl::mappers;
  359. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  360. const auto current = controller->session().data().chatbots().current();
  361. _recipients = Data::BusinessRecipients::MakeValid(current.recipients);
  362. _repliesAllowed = current.repliesAllowed;
  363. AddDividerTextWithLottie(content, {
  364. .lottie = u"robot"_q,
  365. .lottieSize = st::settingsCloudPasswordIconSize,
  366. .lottieMargins = st::peerAppearanceIconPadding,
  367. .showFinished = showFinishes(),
  368. .about = tr::lng_chatbots_about(
  369. lt_link,
  370. tr::lng_chatbots_about_link(
  371. ) | Ui::Text::ToLink(tr::lng_chatbots_info_url(tr::now)),
  372. Ui::Text::WithEntities),
  373. .aboutMargins = st::peerAppearanceCoverLabelMargin,
  374. });
  375. const auto username = content->add(
  376. object_ptr<Ui::InputField>(
  377. content,
  378. st::settingsChatbotsUsername,
  379. tr::lng_chatbots_placeholder(),
  380. (current.bot
  381. ? current.bot->session().createInternalLink(
  382. current.bot->username())
  383. : QString())),
  384. st::settingsChatbotsUsernameMargins);
  385. _usernameValue = DebouncedValue(username);
  386. _botValue = rpl::single(BotState{
  387. current.bot,
  388. current.bot ? LookupState::Ready : LookupState::Empty
  389. }) | rpl::then(
  390. LookupBot(&controller->session(), _usernameValue.changes())
  391. );
  392. const auto resetBot = [=] {
  393. username->setText(QString());
  394. username->setFocus();
  395. };
  396. content->add(object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
  397. content,
  398. MakeBotPreview(content, _botValue.value(), resetBot)));
  399. Ui::AddDividerText(
  400. content,
  401. tr::lng_chatbots_add_about(),
  402. st::peerAppearanceDividerTextMargin);
  403. AddBusinessRecipientsSelector(content, {
  404. .controller = controller,
  405. .title = tr::lng_chatbots_access_title(),
  406. .data = &_recipients,
  407. .type = Data::BusinessRecipientsType::Bots,
  408. });
  409. Ui::AddSkip(content, st::settingsChatbotsAccessSkip);
  410. Ui::AddDividerText(
  411. content,
  412. tr::lng_chatbots_exclude_about(),
  413. st::peerAppearanceDividerTextMargin);
  414. Ui::AddSkip(content);
  415. Ui::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title());
  416. content->add(object_ptr<Ui::SettingsButton>(
  417. content,
  418. tr::lng_chatbots_reply(),
  419. st::settingsButtonNoIcon
  420. ))->toggleOn(_repliesAllowed.value())->toggledChanges(
  421. ) | rpl::start_with_next([=](bool value) {
  422. _repliesAllowed = value;
  423. }, content->lifetime());
  424. Ui::AddSkip(content);
  425. Ui::AddDividerText(
  426. content,
  427. tr::lng_chatbots_reply_about(),
  428. st::settingsChatbotsBottomTextMargin,
  429. RectPart::Top);
  430. Ui::ResizeFitChild(this, content);
  431. }
  432. void Chatbots::save() {
  433. const auto show = controller()->uiShow();
  434. const auto fail = [=](QString error) {
  435. if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) {
  436. show->showToast(tr::lng_greeting_recipients_empty(tr::now));
  437. } else if (error == u"BOT_BUSINESS_MISSING"_q) {
  438. show->showToast(tr::lng_chatbots_not_supported(tr::now));
  439. }
  440. };
  441. controller()->session().data().chatbots().save({
  442. .bot = _botValue.current().bot,
  443. .recipients = _recipients.current(),
  444. .repliesAllowed = _repliesAllowed.current(),
  445. }, [=] {
  446. }, fail);
  447. }
  448. } // namespace
  449. Type ChatbotsId() {
  450. return Chatbots::Id();
  451. }
  452. } // namespace Settings