diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 6651b2477c..273b999533 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -225,6 +225,8 @@ PRIVATE
boxes/peers/channel_ownership_transfer.h
boxes/peers/choose_peer_box.cpp
boxes/peers/choose_peer_box.h
+ boxes/peers/create_managed_bot_box.cpp
+ boxes/peers/create_managed_bot_box.h
boxes/peers/edit_contact_box.cpp
boxes/peers/edit_contact_box.h
boxes/peers/edit_forum_topic_box.cpp
diff --git a/Telegram/Resources/icons/chat/code_tags.svg b/Telegram/Resources/icons/chat/code_tags.svg
new file mode 100644
index 0000000000..f03e9ce1b3
--- /dev/null
+++ b/Telegram/Resources/icons/chat/code_tags.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index c959d4b667..4bfbb5e3b7 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2546,6 +2546,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_new_creator_pending" = "{user} will become the new main admin in 7 days if {from} does not return.";
"lng_action_managed_bot_created" = "{from} created a bot {bot}.";
+"lng_create_bot_title" = "Create Bot";
+"lng_create_bot_subtitle" = "{bot} would like to create and manage a chatbot on your behalf.";
+"lng_create_bot_name_placeholder" = "Bot Name";
+"lng_create_bot_username_placeholder" = "Bot Username";
+"lng_create_bot_username_available" = "{username} is available.";
+"lng_create_bot_username_taken" = "This username is already taken.";
+"lng_create_bot_username_bad_symbols" = "Username can only contain a-z, 0-9, and underscores.";
+"lng_create_bot_username_too_short" = "Username must be at least 5 characters.";
+"lng_create_bot_button" = "Create";
+"lng_managed_bot_label" = "{icon} Created and managed by {bot}.";
+"lng_managed_bot_ready" = "**{name}** is ready!\n\nClick **Start** below to test your new chatbot. Its behavior is defined by **{parent}**.";
+
"lng_stake_game_title" = "Emoji Stake";
"lng_stake_game_beta" = "Beta";
"lng_stake_game_about" = "A limited play-test of the upcoming emoji mini-game platform for a small group of users.";
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index fc02a07dbe..ea764b8e71 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -11,10 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_cloud_password.h"
#include "api/api_send_progress.h"
#include "api/api_suggest_post.h"
-#include "boxes/share_box.h"
-#include "boxes/passcode_box.h"
-#include "boxes/url_auth_box.h"
#include "boxes/peers/choose_peer_box.h"
+#include "boxes/peers/create_managed_bot_box.h"
+#include "boxes/passcode_box.h"
+#include "boxes/share_box.h"
+#include "boxes/url_auth_box.h"
#include "lang/lang_keys.h"
#include "chat_helpers/bot_command.h"
#include "core/core_cloud_password.h"
@@ -40,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
+#include
#include
#include
@@ -546,6 +548,46 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
QVariant::fromValue(context),
});
} break;
+
+ case ButtonType::CreateBot: {
+ HideSingleUseKeyboard(controller, item);
+
+ auto suggestedName = QString();
+ auto suggestedUsername = QString();
+ {
+ auto stream = QDataStream(button->data);
+ stream >> suggestedName >> suggestedUsername;
+ }
+ const auto peer = item->history()->peer;
+ const auto itemId = item->id;
+ const auto id = int32(button->buttonId);
+ const auto bot = item->getMessageBot();
+ if (!bot) {
+ break;
+ }
+ ShowCreateManagedBotBox({
+ .show = controller->uiShow(),
+ .manager = bot,
+ .suggestedName = suggestedName,
+ .suggestedUsername = suggestedUsername,
+ .done = [=](not_null createdBot) {
+ using Flag = MTPmessages_SendBotRequestedPeer::Flag;
+ peer->session().api().request(
+ MTPmessages_SendBotRequestedPeer(
+ MTP_flags(Flag::f_msg_id),
+ peer->input(),
+ MTP_int(itemId),
+ MTPstring(),
+ MTP_int(id),
+ MTP_vector(
+ 1,
+ createdBot->input()))
+ ).done([=](const MTPUpdates &result) {
+ peer->session().api().applyUpdates(result);
+ }).send();
+ },
+ });
+ } break;
}
}
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index bbb0ab721a..de28a78580 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -2111,6 +2111,18 @@ void Updates::feedUpdate(const MTPUpdate &update) {
}
} break;
+ case mtpc_updateManagedBot: {
+ const auto &d = update.c_updateManagedBot();
+ if (const auto user = session().data().userLoaded(
+ UserId(d.vuser_id().v))) {
+ session().api().requestFullPeer(user);
+ }
+ if (const auto bot = session().data().userLoaded(
+ UserId(d.vbot_id().v))) {
+ session().api().requestFullPeer(bot);
+ }
+ } break;
+
case mtpc_updatePendingJoinRequests: {
const auto &d = update.c_updatePendingJoinRequests();
if (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer()))) {
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 6ca5eda121..68d777ce9e 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -1235,3 +1235,29 @@ disableSharingButtonLock: IconEmoji {
icon: icon {{ "emoji/premium_lock", activeButtonFg }};
padding: margins(-2px, 1px, 0px, 0px);
}
+
+createBotBox: Box(defaultBox) {
+ shadowIgnoreTopSkip: true;
+}
+createBotUserpicPadding: margins(0px, 16px, 0px, 10px);
+createBotTitlePadding: margins(0px, 0px, 0px, 8px);
+createBotSubtitlePadding: margins(0px, 0px, 0px, 4px);
+createBotCenteredText: FlatLabel(defaultFlatLabel) {
+ align: align(top);
+ minWidth: 40px;
+}
+createBotFieldSpacing: 8px;
+createBotUsernameField: InputField(defaultInputField) {
+ textMargins: margins(0px, 28px, 0px, 4px);
+}
+createBotUsernamePrefix: FlatLabel(defaultFlatLabel) {
+ style: boxTextStyle;
+}
+createBotUsernameSuffix: FlatLabel(createBotUsernamePrefix) {
+ textFg: windowSubTextFg;
+}
+
+managedBotIconEmoji: IconEmoji {
+ icon: icon {{ "chat/code_tags", windowSubTextFg }};
+ padding: margins(-2px, -2px, -2px, 0px);
+}
diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
index 908579b221..3d55b4aa12 100644
--- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/add_contact_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_limits_box.h"
+#include "chat_helpers/compose/compose_show.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
@@ -37,7 +38,7 @@ class ChoosePeerBoxController final
, public base::has_weak_ptr {
public:
ChoosePeerBoxController(
- not_null navigation,
+ not_null session,
not_null bot,
RequestPeerQuery query,
Fn>)> callback);
@@ -59,7 +60,7 @@ private:
void prepareRestrictions();
- const not_null _navigation;
+ const not_null _session;
not_null _bot;
RequestPeerQuery _query;
base::flat_set> _commonGroups;
@@ -324,12 +325,12 @@ object_ptr CreatePeerByQueryBox(
}
ChoosePeerBoxController::ChoosePeerBoxController(
- not_null navigation,
+ not_null session,
not_null bot,
RequestPeerQuery query,
Fn>)> callback)
-: ChatsListBoxController(&navigation->session())
-, _navigation(navigation)
+: ChatsListBoxController(session)
+, _session(session)
, _bot(bot)
, _query(query)
, _callback(std::move(callback)) {
@@ -339,7 +340,7 @@ ChoosePeerBoxController::ChoosePeerBoxController(
}
Main::Session &ChoosePeerBoxController::session() const {
- return _navigation->session();
+ return *_session;
}
void ChoosePeerBoxController::prepareRestrictions() {
@@ -380,8 +381,16 @@ void ChoosePeerBoxController::prepareRestrictions() {
}, icon->lifetime());
button->setClickedCallback([=] {
- _navigation->parentController()->show(
- CreatePeerByQueryBox(_navigation, _bot, _query, _callback));
+ const auto controller = ChatHelpers::ResolveWindowDefault()(
+ _session);
+ if (controller) {
+ delegate()->peerListUiShow()->showBox(
+ CreatePeerByQueryBox(
+ controller,
+ _bot,
+ _query,
+ _callback));
+ }
});
button->events(
@@ -495,13 +504,26 @@ void ShowChoosePeerBox(
not_null bot,
RequestPeerQuery query,
Fn>)> chosen) {
+ ShowChoosePeerBox(
+ navigation->uiShow(),
+ bot,
+ query,
+ std::move(chosen));
+}
+
+void ShowChoosePeerBox(
+ std::shared_ptr show,
+ not_null bot,
+ RequestPeerQuery query,
+ Fn>)> chosen) {
+ const auto session = &show->session();
const auto needCommonGroups = query.isBotParticipant
&& (query.type == RequestPeerQuery::Type::Group)
&& !query.myRights;
- if (needCommonGroups && !bot->session().api().botCommonGroups(bot)) {
- const auto weak = base::make_weak(navigation);
- bot->session().api().requestBotCommonGroups(bot, [=] {
- if (const auto strong = weak.get()) {
+ if (needCommonGroups && !session->api().botCommonGroups(bot)) {
+ const auto weak = std::weak_ptr(show);
+ session->api().requestBotCommonGroups(bot, [=] {
+ if (const auto strong = weak.lock()) {
ShowChoosePeerBox(strong, bot, query, chosen);
}
});
@@ -517,7 +539,7 @@ void ShowChoosePeerBox(
};
const auto limit = query.maxQuantity;
auto controller = std::make_unique(
- navigation,
+ session,
bot,
query,
std::move(callback));
@@ -540,7 +562,7 @@ void ShowChoosePeerBox(
});
}, box->lifetime());
};
- *weak = navigation->parentController()->show(Box(
+ *weak = show->show(Box(
std::move(controller),
std::move(initBox)));
}
diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h
index 982ec4784c..3ba5268cef 100644
--- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h
+++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h
@@ -9,6 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
struct RequestPeerQuery;
+namespace Main {
+class Session;
+class SessionShow;
+} // namespace Main
+
namespace Ui {
class BoxContent;
} // namespace Ui
@@ -22,3 +27,9 @@ void ShowChoosePeerBox(
not_null bot,
RequestPeerQuery query,
Fn>)> chosen);
+
+void ShowChoosePeerBox(
+ std::shared_ptr show,
+ not_null bot,
+ RequestPeerQuery query,
+ Fn>)> chosen);
diff --git a/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp
new file mode 100644
index 0000000000..68d6cef02d
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.cpp
@@ -0,0 +1,347 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "boxes/peers/create_managed_bot_box.h"
+
+#include "base/timer.h"
+#include "boxes/peers/edit_peer_common.h"
+#include "data/data_session.h"
+#include "data/data_user.h"
+#include "lang/lang_keys.h"
+#include "main/session/session_show.h"
+#include "main/main_session.h"
+#include "mtproto/sender.h"
+#include "ui/controls/userpic_button.h"
+#include "ui/layers/generic_box.h"
+#include "ui/widgets/fields/input_field.h"
+#include "ui/widgets/fields/special_fields.h"
+#include "ui/widgets/labels.h"
+#include "ui/vertical_list.h"
+
+#include "styles/style_boxes.h"
+#include "styles/style_layers.h"
+
+void CreateManagedBotBox(
+ not_null box,
+ CreateManagedBotDescriptor &&descriptor) {
+ struct State {
+ base::Timer checkTimer;
+ mtpRequestId checkRequestId = 0;
+ mtpRequestId createRequestId = 0;
+ QString checkUsername;
+ QString errorText;
+ QString goodText;
+ };
+ const auto show = descriptor.show;
+ const auto session = &show->session();
+ const auto viaDeeplink = descriptor.viaDeeplink;
+ const auto done = std::move(descriptor.done);
+ const auto manager = descriptor.manager;
+
+ const auto api = box->lifetime().make_state(
+ &session->mtp());
+ const auto state = box->lifetime().make_state();
+
+ box->setStyle(st::createBotBox);
+ box->setWidth(st::boxWidth);
+ box->setNoContentMargin(true);
+ box->addTopButton(st::boxTitleClose, [=] {
+ box->closeBox();
+ });
+
+ if (manager) {
+ auto userpic = object_ptr(
+ box,
+ manager,
+ st::defaultUserpicButton);
+ userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
+ box->addRow(
+ std::move(userpic),
+ st::boxRowPadding + st::createBotUserpicPadding,
+ style::al_top);
+ }
+
+ box->addRow(
+ object_ptr(
+ box,
+ tr::lng_create_bot_title(),
+ st::boxTitle),
+ st::boxRowPadding + st::createBotTitlePadding,
+ style::al_top);
+
+ if (manager) {
+ box->addRow(
+ object_ptr(
+ box,
+ tr::lng_create_bot_subtitle(
+ lt_bot,
+ rpl::single(tr::bold(manager->name())),
+ tr::rich),
+ st::createBotCenteredText),
+ st::boxRowPadding + st::createBotSubtitlePadding,
+ style::al_top
+ )->setTryMakeSimilarLines(true);
+ }
+
+ const auto name = box->addRow(object_ptr(
+ box,
+ st::defaultInputField,
+ tr::lng_create_bot_name_placeholder(),
+ descriptor.suggestedName));
+ name->setMaxLength(Ui::EditPeer::kMaxGroupChannelTitle);
+
+ Ui::AddSkip(box->verticalLayout(), st::createBotFieldSpacing);
+
+ const auto botPrefixText = u"@"_q;
+ const auto botSuffixText = u"bot"_q;
+
+ auto initialUsername = descriptor.suggestedUsername;
+ if (initialUsername.startsWith(botPrefixText)) {
+ initialUsername.remove(0, botPrefixText.size());
+ }
+ if (initialUsername.endsWith(botSuffixText, Qt::CaseInsensitive)) {
+ initialUsername.chop(3);
+ }
+
+ const auto fieldSt = box->lifetime().make_state(
+ st::createBotUsernameField);
+ fieldSt->textMargins.setLeft(
+ st::defaultFlatLabel.style.font->width(botPrefixText));
+ fieldSt->textMargins.setRight(
+ st::createBotUsernameSuffix.style.font->width(botSuffixText));
+ fieldSt->placeholderMargins.setLeft(-fieldSt->textMargins.left());
+ fieldSt->placeholderMargins.setRight(-fieldSt->textMargins.right());
+ const auto usernameWrap = box->addRow(object_ptr(box));
+ const auto username = Ui::CreateChild(
+ usernameWrap,
+ *fieldSt,
+ tr::lng_create_bot_username_placeholder(),
+ initialUsername,
+ QString()); // linkPlaceholder
+ usernameWrap->widthValue() | rpl::on_next([=](int width) {
+ username->resizeToWidth(width);
+ }, username->lifetime());
+ username->heightValue() | rpl::on_next([=](int height) {
+ usernameWrap->resize(usernameWrap->width(), height);
+ }, username->lifetime());
+
+ const auto botPrefix = Ui::CreateChild(
+ username,
+ botPrefixText,
+ st::createBotUsernamePrefix);
+ const auto botSuffix = Ui::CreateChild(
+ username,
+ botSuffixText,
+ st::createBotUsernameSuffix);
+ botPrefix->setAttribute(Qt::WA_TransparentForMouseEvents);
+ botPrefix->show();
+ botSuffix->setAttribute(Qt::WA_TransparentForMouseEvents);
+ botSuffix->show();
+
+ const auto updatePositions = [=] {
+ const auto &margin = fieldSt->textMargins;
+ const auto &font = fieldSt->style.font;
+ const auto text = username->getLastText();
+ const auto textWidth = font->width(text);
+ const auto maxX = username->width() - margin.right();
+ const auto x = std::min(margin.left() + textWidth, maxX);
+ botPrefix->move(0, margin.top());
+ botSuffix->move(x, margin.top());
+ };
+
+ username->geometryValue(
+ ) | rpl::on_next(updatePositions, username->lifetime());
+
+ const auto statusWrapper = box->addRow(
+ object_ptr(box),
+ st::boxRowPadding + QMargins(0, st::defaultVerticalListSkip, 0, 0));
+
+ const auto statusLabel = Ui::CreateChild(
+ statusWrapper,
+ st::aboutRevokePublicLabel);
+ statusLabel->move(0, 0);
+
+ const auto maxHeight = box->lifetime().make_state(0);
+ statusLabel->heightValue(
+ ) | rpl::on_next([=](int height) {
+ const auto newMax = std::max({
+ *maxHeight,
+ height,
+ st::aboutRevokePublicLabel.style.font->height,
+ });
+ if (*maxHeight != newMax) {
+ *maxHeight = newMax;
+ statusWrapper->resize(statusWrapper->width(), newMax);
+ }
+ }, statusWrapper->lifetime());
+
+ statusWrapper->widthValue(
+ ) | rpl::on_next([=](int width) {
+ statusLabel->resizeToWidth(width);
+ }, statusWrapper->lifetime());
+
+ box->setFocusCallback([=] { name->setFocusFast(); });
+
+ const auto setError = [=](const QString &text) {
+ state->errorText = text;
+ state->goodText = QString();
+ statusLabel->setText(text);
+ statusLabel->setTextColorOverride(
+ text.isEmpty()
+ ? std::optional()
+ : st::boxTextFgError->c);
+ state->checkTimer.cancel();
+ };
+
+ const auto checkUsername = [=] {
+ api->request(base::take(state->checkRequestId)).cancel();
+
+ auto raw = username->getLastText().trimmed();
+ if (raw.startsWith(QChar('@'))) {
+ raw = raw.mid(1);
+ }
+ const auto value = raw + u"bot"_q;
+ if (value.size() < Ui::EditPeer::kMinUsernameLength) {
+ return;
+ }
+ state->checkUsername = value;
+ state->checkRequestId = api->request(MTPbots_CheckUsername(
+ MTP_string(value)
+ )).done([=](const MTPBool &result) {
+ state->checkRequestId = 0;
+ if (mtpIsTrue(result)) {
+ state->errorText = QString();
+ state->goodText = tr::lng_create_bot_username_available(
+ tr::now,
+ lt_username,
+ state->checkUsername);
+ statusLabel->setText(state->goodText);
+ statusLabel->setTextColorOverride(st::boxTextFgGood->c);
+ } else {
+ setError(tr::lng_create_bot_username_taken(tr::now));
+ }
+ }).fail([=](const MTP::Error &error) {
+ state->checkRequestId = 0;
+ setError(tr::lng_create_bot_username_taken(tr::now));
+ }).send();
+ };
+
+ state->checkTimer.setCallback(checkUsername);
+
+ const auto usernameChanged = [=] {
+ const auto value = username->getLastText().trimmed();
+ if (value.isEmpty()) {
+ setError(QString());
+ return;
+ }
+
+ const auto len = int(value.size());
+ for (auto i = 0; i < len; ++i) {
+ const auto ch = value.at(i);
+ if ((ch < 'A' || ch > 'Z')
+ && (ch < 'a' || ch > 'z')
+ && (ch < '0' || ch > '9')
+ && ch != '_') {
+ setError(
+ tr::lng_create_bot_username_bad_symbols(tr::now));
+ return;
+ }
+ }
+
+ if ((value + u"bot"_q).size() < Ui::EditPeer::kMinUsernameLength) {
+ setError(tr::lng_create_bot_username_too_short(tr::now));
+ return;
+ }
+
+ setError(QString());
+ state->checkTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout);
+ };
+
+ const auto submit = [=] {
+ if (state->createRequestId) {
+ return;
+ }
+
+ const auto nameValue = name->getLastText().trimmed();
+ if (nameValue.isEmpty()) {
+ name->showError();
+ return;
+ }
+
+ auto rawUsername = username->getLastText().trimmed();
+ if (rawUsername.startsWith(QChar('@'))) {
+ rawUsername = rawUsername.mid(1);
+ }
+ const auto usernameValue = rawUsername + u"bot"_q;
+ if (rawUsername.isEmpty()) {
+ username->showError();
+ return;
+ }
+
+ if (!state->errorText.isEmpty() || state->goodText.isEmpty()) {
+ username->showError();
+ return;
+ }
+
+ using Flag = MTPbots_CreateBot::Flag;
+ const auto flags = viaDeeplink ? Flag::f_via_deeplink : Flag(0);
+ const auto managerInput = manager
+ ? manager->inputUser()
+ : MTP_inputUserEmpty();
+
+ const auto weak = base::make_weak(box);
+ state->createRequestId = api->request(MTPbots_CreateBot(
+ MTP_flags(flags),
+ MTP_string(nameValue),
+ MTP_string(usernameValue),
+ managerInput
+ )).done([=](const MTPUser &result) {
+ state->createRequestId = 0;
+ const auto user = session->data().processUser(result);
+ if (done) {
+ done(user);
+ }
+ if (const auto strong = weak.get()) {
+ strong->closeBox();
+ }
+ }).fail([=](const MTP::Error &error) {
+ state->createRequestId = 0;
+ const auto type = error.type();
+ if (type == u"USERNAME_OCCUPIED"_q
+ || type == u"USERNAME_INVALID"_q) {
+ username->showError();
+ setError(tr::lng_create_bot_username_taken(tr::now));
+ } else {
+ show->showToast(type);
+ }
+ }).send();
+ };
+
+ QObject::connect(username, &Ui::UsernameInput::changed, [=] {
+ usernameChanged();
+ updatePositions();
+ });
+
+ name->submits(
+ ) | rpl::on_next([=](auto) {
+ username->setFocus();
+ }, name->lifetime());
+
+ QObject::connect(username, &Ui::UsernameInput::submitted, submit);
+
+ box->addButton(tr::lng_create_bot_button(), submit);
+ box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+
+ if (!descriptor.suggestedUsername.isEmpty()) {
+ usernameChanged();
+ }
+}
+
+void ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor) {
+ const auto show = descriptor.show;
+ show->showBox(Box(CreateManagedBotBox, std::move(descriptor)));
+}
diff --git a/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h
new file mode 100644
index 0000000000..d7bb08f334
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/peers/create_managed_bot_box.h
@@ -0,0 +1,33 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+class UserData;
+
+namespace Main {
+class SessionShow;
+} // namespace Main
+
+namespace Ui {
+class GenericBox;
+} // namespace Ui
+
+struct CreateManagedBotDescriptor {
+ std::shared_ptr show;
+ UserData *manager = nullptr;
+ QString suggestedName;
+ QString suggestedUsername;
+ bool viaDeeplink = false;
+ Fn)> done;
+};
+
+void CreateManagedBotBox(
+ not_null box,
+ CreateManagedBotDescriptor &&descriptor);
+
+void ShowCreateManagedBotBox(CreateManagedBotDescriptor &&descriptor);
diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp
index cbcd6037ff..3659cac18e 100644
--- a/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp
+++ b/Telegram/SourceFiles/core/deep_links/deep_links_new.cpp
@@ -7,6 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "core/deep_links/deep_links_router.h"
+#include "apiwrap.h"
+#include "boxes/peers/create_managed_bot_box.h"
+#include "data/data_peer_id.h"
+#include "data/data_session.h"
+#include "data/data_user.h"
+#include "main/main_session.h"
#include "window/window_session_controller.h"
namespace Core::DeepLinks {
@@ -36,6 +42,54 @@ Result ShowAddContact(const Context &ctx) {
return Result::Handled;
}
+Result ShowNewBot(const Context &ctx) {
+ if (!ctx.controller) {
+ return Result::NeedsAuth;
+ }
+ const auto manager = ctx.params.value(u"manager"_q);
+ const auto username = ctx.params.value(u"username"_q);
+ const auto title = ctx.params.value(u"name"_q);
+ if (manager.isEmpty()) {
+ return Result::Handled;
+ }
+ const auto session = &ctx.controller->session();
+ const auto weak = base::make_weak(ctx.controller);
+ session->api().request(MTPcontacts_ResolveUsername(
+ MTP_flags(0),
+ MTP_string(manager),
+ MTP_string()
+ )).done([=](const MTPcontacts_ResolvedPeer &result) {
+ const auto strong = weak.get();
+ if (!strong) {
+ return;
+ }
+ result.match([&](const MTPDcontacts_resolvedPeer &data) {
+ strong->session().data().processUsers(data.vusers());
+ strong->session().data().processChats(data.vchats());
+ const auto peerId = peerFromMTP(data.vpeer());
+ if (const auto managerBot = strong->session().data().userLoaded(
+ peerToUser(peerId))) {
+ if (!managerBot->isBot()) {
+ return;
+ }
+ ShowCreateManagedBotBox({
+ .show = strong->uiShow(),
+ .manager = managerBot,
+ .suggestedName = title,
+ .suggestedUsername = username,
+ .viaDeeplink = true,
+ .done = [weak](not_null createdBot) {
+ if (const auto strong = weak.get()) {
+ strong->showPeerHistory(createdBot);
+ }
+ },
+ });
+ }
+ });
+ }).send();
+ return Result::Handled;
+}
+
} // namespace
void RegisterNewHandlers(Router &router) {
@@ -53,6 +107,11 @@ void RegisterNewHandlers(Router &router) {
.path = u"contact"_q,
.action = CodeBlock{ ShowAddContact },
});
+
+ router.add(u"newbot"_q, {
+ .path = QString(),
+ .action = CodeBlock{ ShowNewBot },
+ });
}
} // namespace Core::DeepLinks
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index c35238cfbe..fa1e04bfe0 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -1887,6 +1887,18 @@ QString TryConvertUrlToLocal(QString url) {
} else if (const auto callMatch = regex_match(u"^call/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
const auto slug = callMatch->captured(1);
return u"tg://call?slug="_q + slug;
+ } else if (const auto newbotMatch = regex_match(u"^newbot/([a-zA-Z0-9\\.\\_]+)(/([a-zA-Z0-9\\.\\_]+))?(\\?(.+))?$"_q, query, matchOptions)) {
+ const auto manager = newbotMatch->captured(1);
+ const auto username = newbotMatch->captured(3);
+ const auto params = newbotMatch->captured(5);
+ auto result = u"tg://newbot?manager="_q + url_encode(manager);
+ if (!username.isEmpty()) {
+ result += u"&username="_q + url_encode(username);
+ }
+ if (!params.isEmpty()) {
+ result += '&' + params;
+ }
+ return result;
} else if (const auto privateMatch = regex_match(u"^"
"c/(\\-?\\d+)"
"("
diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp
index 86c23814be..0300436a0e 100644
--- a/Telegram/SourceFiles/data/data_user.cpp
+++ b/Telegram/SourceFiles/data/data_user.cpp
@@ -318,6 +318,14 @@ void UserData::setPersonalChannel(ChannelId channelId, MsgId messageId) {
}
}
+UserId UserData::botManagerId() const {
+ return _botManagerId;
+}
+
+void UserData::setBotManagerId(UserId managerId) {
+ _botManagerId = managerId;
+}
+
MTPInputUser UserData::inputUser() const {
const auto item = isLoaded() ? nullptr : owner().messageWithPeer(id);
if (item) {
@@ -993,6 +1001,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) {
user->setPersonalChannel(
update.vpersonal_channel_id().value_or_empty(),
update.vpersonal_channel_message().value_or_empty());
+ user->setBotManagerId(update.vbot_manager_id().value_or_empty());
if (user->isSelf()) {
user->owner().businessInfo().applyAwaySettings(
FromMTP(&user->owner(), update.vbusiness_away_message()));
diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h
index e969efc4b6..bde10b135b 100644
--- a/Telegram/SourceFiles/data/data_user.h
+++ b/Telegram/SourceFiles/data/data_user.h
@@ -291,6 +291,9 @@ public:
[[nodiscard]] MsgId personalChannelMessageId() const;
void setPersonalChannel(ChannelId channelId, MsgId messageId);
+ [[nodiscard]] UserId botManagerId() const;
+ void setBotManagerId(UserId managerId);
+
[[nodiscard]] MTPInputUser inputUser() const;
QString firstName;
@@ -334,6 +337,7 @@ private:
ChannelId _personalChannelId = 0;
MsgId _personalChannelMessageId = 0;
+ UserId _botManagerId = 0;
uint64 _accessHash = 0;
static constexpr auto kInaccessibleAccessHashOld
diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp
index d2aa2f7b27..92c75259b3 100644
--- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp
+++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp
@@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_components.h"
#include "inline_bots/bot_attach_web_view.h"
+#include
+
namespace {
[[nodiscard]] HistoryMessageMarkupButton::Visual ParseVisual(
@@ -36,7 +38,9 @@ namespace {
};
}
-[[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL(
+} // namespace
+
+RequestPeerQuery RequestPeerQueryFromTL(
const MTPDkeyboardButtonRequestPeer &query) {
using Type = RequestPeerQuery::Type;
using Restriction = RequestPeerQuery::Restriction;
@@ -75,8 +79,6 @@ namespace {
return result;
}
-} // namespace
-
InlineBots::PeerTypes PeerTypesFromMTP(
const MTPvector &types) {
using namespace InlineBots;
@@ -170,16 +172,38 @@ void HistoryMessageMarkupData::fillRows(
qs(data.vtext()),
ParseVisual(data.vstyle()));
}, [&](const MTPDkeyboardButtonRequestPeer &data) {
- const auto query = RequestPeerQueryFromTL(data);
- row.emplace_back(
- Type::RequestPeer,
- qs(data.vtext()),
- ParseVisual(data.vstyle()),
- QByteArray(
- reinterpret_cast(&query),
- sizeof(query)),
- QString(),
- int64(data.vbutton_id().v));
+ data.vpeer_type().match([&](
+ const MTPDrequestPeerTypeCreateBot &createData) {
+ auto serialized = QByteArray();
+ {
+ auto stream = QDataStream(
+ &serialized,
+ QIODevice::WriteOnly);
+ stream
+ << qs(createData.vsuggested_name()
+ .value_or_empty())
+ << qs(createData.vsuggested_username()
+ .value_or_empty());
+ }
+ row.emplace_back(
+ Type::CreateBot,
+ qs(data.vtext()),
+ ParseVisual(data.vstyle()),
+ serialized,
+ QString(),
+ int64(data.vbutton_id().v));
+ }, [&](const auto &) {
+ const auto query = RequestPeerQueryFromTL(data);
+ row.emplace_back(
+ Type::RequestPeer,
+ qs(data.vtext()),
+ ParseVisual(data.vstyle()),
+ QByteArray(
+ reinterpret_cast(&query),
+ sizeof(query)),
+ QString(),
+ int64(data.vbutton_id().v));
+ });
}, [&](const MTPDkeyboardButtonUrl &data) {
row.emplace_back(
Type::Url,
diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h
index 2614b20c87..8df3597074 100644
--- a/Telegram/SourceFiles/history/history_item_reply_markup.h
+++ b/Telegram/SourceFiles/history/history_item_reply_markup.h
@@ -69,6 +69,11 @@ struct RequestPeerQuery {
};
static_assert(std::is_trivially_copy_assignable_v);
+class MTPDkeyboardButtonRequestPeer;
+
+[[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL(
+ const MTPDkeyboardButtonRequestPeer &query);
+
struct HistoryMessageMarkupButton {
enum class Type : uchar {
Default,
@@ -92,6 +97,7 @@ struct HistoryMessageMarkupButton {
SuggestDecline,
SuggestAccept,
SuggestChange,
+ CreateBot,
};
enum class Color : uchar {
diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp
index 544be0cace..3bd6f439a3 100644
--- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp
@@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_about_view.h"
+#include "api/api_peer_colors.h"
#include "api/api_premium.h"
#include "api/api_sending.h"
#include "apiwrap.h"
#include "base/random.h"
+#include "base/unixtime.h"
#include "ui/effects/premium_stars.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/stickers_lottie.h"
@@ -21,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_document.h"
+#include "data/data_emoji_statuses.h"
+#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/history_view_group_call_bar.h"
@@ -40,15 +44,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/sections/settings_credits.h" // BuyStarsHandler
#include "settings/sections/settings_premium.h"
#include "ui/chat/chat_style.h"
+#include "ui/image/image_location_factory.h"
#include "ui/text/custom_emoji_instance.h"
#include "ui/text/text_utilities.h"
#include "ui/text/text_options.h"
#include "ui/dynamic_image.h"
+#include "ui/empty_userpic.h"
#include "ui/painter.h"
+#include "ui/top_background_gradient.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" // GroupCallUserpics
#include "styles/style_credits.h"
+#include "styles/style_menu_icons.h"
namespace HistoryView {
namespace {
@@ -603,6 +611,83 @@ bool EmptyChatLockedBox::hasHeavyPart() {
void EmptyChatLockedBox::unloadHeavyPart() {
}
+QImage GenerateManagedBotImage(not_null user) {
+ auto centerColor = QColor();
+ auto edgeColor = QColor();
+ if (const auto collectible = user->emojiStatusId().collectible) {
+ centerColor = collectible->centerColor;
+ edgeColor = collectible->edgeColor;
+ } else if (const auto color
+ = user->session().api().peerColors().colorProfileFor(user)) {
+ if (color->bg.size() > 1) {
+ centerColor = color->bg[1];
+ edgeColor = color->bg[0];
+ }
+ }
+ if (!centerColor.isValid()) {
+ const auto colorIndex = Ui::EmptyUserpic::ColorIndex(
+ user->id.value);
+ const auto colors = Ui::EmptyUserpic::UserpicColor(colorIndex);
+ centerColor = colors.color1->c;
+ edgeColor = colors.color2->c;
+ }
+
+ const auto size = QSize(
+ st::managedBotImageWidth,
+ st::managedBotImageHeight);
+ auto image = Ui::CreateTopBgGradient(
+ size,
+ centerColor,
+ edgeColor,
+ false);
+ if (image.isNull()) {
+ return image;
+ }
+
+ auto p = QPainter(&image);
+ auto hq = PainterHighQualityEnabler(p);
+
+ auto iconColor = edgeColor.toHsv();
+ iconColor.setHsv(
+ iconColor.hsvHue(),
+ iconColor.hsvSaturation(),
+ std::max(iconColor.value() - 64, 0));
+ iconColor = iconColor.toRgb();
+ const auto width = size.width();
+ const auto height = size.height();
+ const auto &icon = st::menuIconBot;
+ const auto &points = Ui::PatternBgPoints();
+ for (const auto &point : points) {
+ const auto cx = point.position.x() * width;
+ const auto cy = point.position.y() * height;
+ p.save();
+ p.setOpacity(point.opacity);
+ if (point.scale < 1.) {
+ p.translate(cx, cy);
+ p.scale(point.scale, point.scale);
+ p.translate(-cx, -cy);
+ }
+ const auto x = int(cx) - icon.width() / 2;
+ const auto y = int(cy) - icon.height() / 2;
+ icon.paint(p, x, y, width, iconColor);
+ p.restore();
+ }
+
+ const auto ratio = style::DevicePixelRatio();
+ const auto iheight = st::managedBotCodeIcon.height();
+ const auto scale = (size.height() * ratio * 100) / iheight;
+ auto iconImage = st::managedBotCodeIcon.instance(Qt::white, scale, true);
+ iconImage.setDevicePixelRatio(ratio);
+ const auto iw = iconImage.width() / ratio;
+ const auto ih = iconImage.height() / ratio;
+ p.drawImage(
+ QRect((width - iw) / 2, (height - ih) / 2, iw, ih),
+ iconImage);
+
+ p.end();
+ return image;
+}
+
} // namespace
AboutView::AboutView(
@@ -704,6 +789,12 @@ bool AboutView::refresh() {
}
setItem(makeNewBotThread(), nullptr);
return true;
+ } else if (user->botManagerId() && info->description.isEmpty()) {
+ if (_item) {
+ return false;
+ }
+ setItem(makeManagedBotInfo(user), nullptr);
+ return true;
}
const auto version = info->descriptionVersion;
if (_version == version) {
@@ -1062,4 +1153,41 @@ AdminLog::OwnedItem AboutView::makeNewBotThread() {
return result;
}
+AdminLog::OwnedItem AboutView::makeManagedBotInfo(
+ not_null user) {
+ const auto image = GenerateManagedBotImage(user);
+ const auto photoImage = image.isNull()
+ ? ImageWithLocation()
+ : Images::FromImageInMemory(image, "PNG");
+ const auto photo = _history->session().data().photo(
+ base::RandomValue(),
+ uint64(0),
+ QByteArray(),
+ base::unixtime::now(),
+ 0,
+ false,
+ QByteArray(),
+ ImageWithLocation{},
+ photoImage,
+ photoImage,
+ ImageWithLocation{},
+ ImageWithLocation{},
+ crl::time(0));
+
+ const auto managerId = user->botManagerId();
+ const auto managerUser = user->owner().userLoaded(managerId);
+ const auto parentName = managerUser
+ ? managerUser->name()
+ : QString();
+ const auto text = tr::lng_managed_bot_ready(
+ tr::now,
+ lt_name,
+ tr::bold(user->name()),
+ lt_parent,
+ tr::bold(parentName),
+ tr::rich);
+
+ return makeAboutSimple(text, nullptr, photo);
+}
+
} // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h
index e9ddfb2fae..e9fed1829a 100644
--- a/Telegram/SourceFiles/history/view/history_view_about_view.h
+++ b/Telegram/SourceFiles/history/view/history_view_about_view.h
@@ -53,6 +53,8 @@ private:
not_null user);
[[nodiscard]] AdminLog::OwnedItem makeBlocked();
[[nodiscard]] AdminLog::OwnedItem makeNewBotThread();
+ [[nodiscard]] AdminLog::OwnedItem makeManagedBotInfo(
+ not_null user);
void makeIntro(not_null user);
void setItem(AdminLog::OwnedItem item, DocumentData *sticker);
void setHelloChosen(not_null sticker);
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index f3d69fc137..62f63e4306 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -1165,7 +1165,8 @@ Element::Element(
if (user
&& user->isBot()
&& !user->isRepliesChat()
- && !user->isVerifyCodes()) {
+ && !user->isVerifyCodes()
+ && !user->botManagerId()) {
AddComponents(FakeBotAboutTop::Bit());
}
}
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index 43c71b0e2b..6d4eb7dc9d 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -208,12 +208,15 @@ QSize Photo::countOptimalSize() {
auto maxWidth = qMax(maxActualWidth, scaled.height());
auto minHeight = qMax(scaled.height(), st::minPhotoSize);
if (_parent->hasBubble()) {
+ const auto botTop = _parent->Get();
const auto captionMaxWidth = _parent->textualMaxWidth();
- const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
- maxWidth = qMin(qMax(maxWidth, maxWithCaption), st::msgMaxWidth);
- minHeight = adjustHeightForLessCrop(
- dimensions,
- { maxWidth, minHeight });
+ if (botTop || !_parent->data()->isFakeAboutView()) {
+ const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
+ maxWidth = qMin(qMax(maxWidth, maxWithCaption), st::msgMaxWidth);
+ minHeight = adjustHeightForLessCrop(
+ dimensions,
+ { maxWidth, minHeight });
+ }
}
return { maxWidth, minHeight };
}
@@ -246,11 +249,15 @@ QSize Photo::countCurrentSize(int newWidth) {
if (botTop) {
accumulate_max(captionMaxWidth, botTop->maxWidth);
}
- const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
- newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
- newHeight = adjustHeightForLessCrop(
- dimensions,
- { newWidth, newHeight });
+ if (botTop || !_parent->data()->isFakeAboutView()) {
+ const auto maxWithCaption = qMin(
+ st::msgMaxWidth,
+ captionMaxWidth);
+ newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
+ newHeight = adjustHeightForLessCrop(
+ dimensions,
+ { newWidth, newHeight });
+ }
}
if (newWidth >= maxWidth()) {
newHeight = qMin(newHeight, minHeight());
@@ -529,8 +536,8 @@ QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const {
blurred = thumbnail;
} else if (const auto small = _dataMedia->image(Size::Small)) {
blurred = small;
- } else {
- blurred = large;
+ } else {
+ blurred = large;
}
const auto resize = large
? ::Media::Streaming::DecideFrameResize(outer, large->size())
@@ -539,9 +546,9 @@ QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const {
}
void Photo::paintUserpicFrame(
- Painter &p,
- QPoint photoPosition,
- bool markFrameShown) const {
+ Painter &p,
+ QPoint photoPosition,
+ bool markFrameShown) const {
const auto autoplay = _data->videoCanBePlayed()
&& videoAutoplayEnabled();
const auto startPlay = autoplay && !_streamed;
@@ -596,9 +603,9 @@ void Photo::paintUserpicFrame(
}
void Photo::paintUserpicFrame(
- Painter &p,
- const PaintContext &context,
- QPoint photoPosition) const {
+ Painter &p,
+ const PaintContext &context,
+ QPoint photoPosition) const {
paintUserpicFrame(p, photoPosition, !context.paused);
if (_data->videoCanBePlayed() && !_streamed) {
@@ -628,6 +635,9 @@ void Photo::paintUserpicFrame(
QSize Photo::photoSize() const {
if (_storyId) {
return { kStoryWidth, kStoryHeight };
+ } else if (_parent->data()->isFakeAboutView()
+ && !_parent->Get()) {
+ return { st::managedBotImageWidth, st::managedBotImageHeight };
}
return QSize(_data->width(), _data->height());
}
diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
index 55c9dac807..ebd134c918 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -79,6 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "ui/text/format_values.h"
+#include "ui/text/text_utilities.h"
#include "ui/text/text_variant.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
@@ -95,6 +96,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h" // Window::Controller::show.
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
+#include "styles/style_boxes.h"
#include "styles/style_channel_earn.h" // st::channelEarnCurrencyCommonMargins
#include "styles/style_info.h"
#include "styles/style_layers.h"
@@ -1218,7 +1220,7 @@ public:
private:
object_ptr setupPersonalChannel(not_null user);
object_ptr setupInfo();
- void setupMainApp();
+ void setupMainApp(bool suppressBottom = false);
void setupBotPermissions();
void addShowTopicsListButton(
Ui::MultiSlideTracker &tracker,
@@ -2124,7 +2126,7 @@ object_ptr DetailsFiller::setupPersonalChannel(
return result;
}
-void DetailsFiller::setupMainApp() {
+void DetailsFiller::setupMainApp(bool suppressBottom) {
const auto button = _wrap->add(
object_ptr(
_wrap,
@@ -2148,6 +2150,9 @@ void DetailsFiller::setupMainApp() {
});
const auto url = tr::lng_mini_apps_tos_url(tr::now);
+ const auto parts = suppressBottom
+ ? RectPart::Top
+ : (RectPart::Top | RectPart::Bottom);
const auto divider = Ui::AddDividerText(
_wrap,
rpl::combine(
@@ -2163,7 +2168,10 @@ void DetailsFiller::setupMainApp() {
text = text.append(u"\n\n"_q).append(verify->description);
}
return text;
- }));
+ }),
+ st::defaultBoxDividerLabelPadding,
+ st::defaultDividerLabel,
+ parts);
divider->setClickHandlerFilter([=](const auto &...) {
UrlClickHandler::Open(url);
return false;
@@ -2370,12 +2378,48 @@ object_ptr DetailsFiller::fill() {
if (const auto info = user->botInfo.get()) {
if (info->hasMainApp) {
_dividerOverridden.force_assign(true);
- setupMainApp();
+ const auto managedBotFollows = user->botManagerId()
+ && !info->canManageEmojiStatus
+ && user->owner().userLoaded(user->botManagerId());
+ setupMainApp(managedBotFollows);
}
if (info->canManageEmojiStatus) {
_dividerOverridden.force_assign(false);
setupBotPermissions();
}
+ if (user->botManagerId()) {
+ if (const auto managerUser = user->owner().userLoaded(
+ user->botManagerId())) {
+ if (!info->hasMainApp) {
+ _dividerOverridden.force_assign(true);
+ }
+ const auto botUsername = managerUser->username();
+ const auto linkText = botUsername.isEmpty()
+ ? managerUser->name()
+ : (u"@"_q + botUsername);
+ const auto parts = (info->hasMainApp && !info->canManageEmojiStatus)
+ ? RectPart::Bottom
+ : (RectPart::Top | RectPart::Bottom);
+ const auto divider = Ui::AddDividerText(
+ _wrap,
+ tr::lng_managed_bot_label(
+ lt_icon,
+ rpl::single(Ui::Text::IconEmoji(&st::managedBotIconEmoji)),
+ lt_bot,
+ rpl::single(tr::link(linkText)),
+ tr::marked),
+ st::defaultBoxDividerLabelPadding,
+ st::defaultDividerLabel,
+ parts);
+ const auto weak = base::make_weak(_controller);
+ divider->setClickHandlerFilter([=](const auto &...) {
+ if (const auto strong = weak.get()) {
+ strong->showPeerInfo(managerUser);
+ }
+ return false;
+ });
+ }
+ }
}
if (!user->isSelf() && !_sublist) {
addReportReaction(_mainTracker, &lastButtonTracker);
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index aef877d07a..b4570a5f8d 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/random.h"
#include "base/timer_rpl.h"
#include "base/unixtime.h"
+#include "boxes/peers/choose_peer_box.h"
+#include "boxes/peers/create_managed_bot_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_preview_box.h"
#include "boxes/share_box.h"
@@ -43,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
+#include "history/history_item_reply_markup.h"
#include "info/bot/starref/info_bot_starref_common.h" // MakePeerBubbleButton
#include "info/profile/info_profile_values.h"
#include "inline_bots/inline_bot_result.h"
@@ -1975,6 +1978,75 @@ void WebViewInstance::botSendPreparedMessage(
}).send();
}
+void WebViewInstance::botRequestChat(
+ Ui::BotWebView::RequestChatRequest request) {
+ const auto bot = _bot;
+ const auto callback = request.callback;
+ const auto requestId = request.requestId;
+ if (!_panel) {
+ callback(u"UNKNOWN_ERROR"_q);
+ return;
+ }
+ const auto show = uiShow();
+ bot->session().api().request(MTPbots_GetRequestedWebViewButton(
+ bot->inputUser(),
+ MTP_string(requestId)
+ )).done([show, bot, callback, requestId](
+ const MTPKeyboardButton &result) {
+ result.match([&](const MTPDkeyboardButtonRequestPeer &data) {
+ if (!*show) {
+ callback(u"UNKNOWN_ERROR"_q);
+ return;
+ }
+ const auto buttonId = data.vbutton_id();
+ const auto sendPeers = [=](
+ std::vector> peers) {
+ using Flag = MTPmessages_SendBotRequestedPeer::Flag;
+ bot->session().api().request(
+ MTPmessages_SendBotRequestedPeer(
+ MTP_flags(Flag::f_webapp_req_id),
+ bot->input(),
+ MTPint(),
+ MTP_string(requestId),
+ buttonId,
+ MTP_vector_from_range(
+ peers | ranges::views::transform([](
+ not_null peer) {
+ return MTPInputPeer(peer->input());
+ })))
+ ).done([=](const MTPUpdates &result) {
+ bot->session().api().applyUpdates(result);
+ callback(QString());
+ }).fail([callback](const MTP::Error &error) {
+ callback(error.type());
+ }).send();
+ };
+ data.vpeer_type().match([&](
+ const MTPDrequestPeerTypeCreateBot &createData) {
+ ShowCreateManagedBotBox({
+ .show = show,
+ .manager = bot,
+ .suggestedName = qs(
+ createData.vsuggested_name().value_or_empty()),
+ .suggestedUsername = qs(
+ createData.vsuggested_username()
+ .value_or_empty()),
+ .done = [=](not_null createdBot) {
+ sendPeers({ createdBot });
+ },
+ });
+ }, [&](const auto &) {
+ const auto query = RequestPeerQueryFromTL(data);
+ ShowChoosePeerBox(show, bot, query, sendPeers);
+ });
+ }, [&](const auto &) {
+ callback(u"UNSUPPORTED_BUTTON_TYPE"_q);
+ });
+ }).fail([callback](const MTP::Error &error) {
+ callback(error.type());
+ }).send();
+}
+
void WebViewInstance::botSetEmojiStatus(
Ui::BotWebView::SetEmojiStatusRequest request) {
const auto bot = _bot;
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index 422219c221..f1c35dafd0 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -300,6 +300,8 @@ private:
Ui::BotWebView::CustomMethodRequest request) override;
void botSendPreparedMessage(
Ui::BotWebView::SendPreparedMessageRequest request) override;
+ void botRequestChat(
+ Ui::BotWebView::RequestChatRequest request) override;
void botSetEmojiStatus(
Ui::BotWebView::SetEmojiStatusRequest request) override;
void botDownloadFile(
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index 14626742ad..70daf5c353 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -1027,6 +1027,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) {
processBottomBarColor(arguments);
} else if (command == "web_app_send_prepared_message") {
processSendMessageRequest(arguments);
+ } else if (command == "web_app_request_chat") {
+ processRequestChat(arguments);
} else if (command == "web_app_set_emoji_status") {
processEmojiStatusRequest(arguments);
} else if (command == "web_app_request_emoji_status_access") {
@@ -1214,6 +1216,34 @@ void Panel::processSendMessageRequest(const QJsonObject &args) {
});
}
+void Panel::processRequestChat(const QJsonObject &args) {
+ if (args.isEmpty()) {
+ _delegate->botClose();
+ return;
+ }
+ const auto requestId = args["req_id"].toString();
+ if (requestId.isEmpty()) {
+ return;
+ }
+ auto callback = crl::guard(this, [=](QString error) {
+ if (error.isEmpty()) {
+ postEvent(
+ "requested_chat_sent",
+ u"{ req_id: \"%1\" }"_q.arg(requestId));
+ } else {
+ postEvent(
+ "requested_chat_failed",
+ u"{ req_id: \"%1\", error: \"%2\" }"_q.arg(
+ requestId,
+ error));
+ }
+ });
+ _delegate->botRequestChat({
+ .requestId = requestId,
+ .callback = std::move(callback),
+ });
+}
+
void Panel::processEmojiStatusRequest(const QJsonObject &args) {
if (args.isEmpty()) {
_delegate->botClose();
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
index d698e9f0fd..5185d80d37 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
@@ -79,6 +79,11 @@ struct SendPreparedMessageRequest {
Fn callback;
};
+struct RequestChatRequest {
+ QString requestId;
+ Fn callback;
+};
+
class Delegate {
public:
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
@@ -111,6 +116,7 @@ public:
virtual void botDownloadFile(DownloadFileRequest request) = 0;
virtual void botSendPreparedMessage(
SendPreparedMessageRequest request) = 0;
+ virtual void botRequestChat(RequestChatRequest request) = 0;
virtual void botVerifyAge(int age) = 0;
virtual void botOpenPrivacyPolicy() = 0;
virtual void botClose() = 0;
@@ -176,6 +182,7 @@ private:
void sendDataMessage(const QJsonObject &args);
void switchInlineQueryMessage(const QJsonObject &args);
void processSendMessageRequest(const QJsonObject &args);
+ void processRequestChat(const QJsonObject &args);
void processEmojiStatusRequest(const QJsonObject &args);
void processEmojiStatusAccessRequest();
void processStorageSaveKey(const QJsonObject &args);
diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style
index d3228885a3..ccba464478 100644
--- a/Telegram/SourceFiles/ui/chat/chat.style
+++ b/Telegram/SourceFiles/ui/chat/chat.style
@@ -1551,6 +1551,10 @@ newThreadAboutIconSkip: chatIntroTitleMargin.top;
newThreadAboutIconOuter: 60px;
newThreadAboutIcon: icon{{ "chat/new_thread-40x40", windowFgActive }};
+managedBotCodeIcon: icon{{ "chat/code_tags", windowFg }};
+managedBotImageWidth: 240px;
+managedBotImageHeight: 120px;
+
stakeBox: Box(defaultBox) {
buttonPadding: margins(24px, 12px, 24px, 24px);
buttonHeight: 42px;
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index a0d830f2bd..bcd1aaf92c 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit a0d830f2bd39b215545001431f4ce581d28e676b
+Subproject commit bcd1aaf92c80588a6d015521eb9ce662e3731d9a