Implement preview and save of chatbots.

This commit is contained in:
John Preston 2024-03-05 20:52:14 +04:00
parent ea36345eee
commit e3f6c189a7
13 changed files with 536 additions and 176 deletions

View file

@ -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";

View file

@ -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(session) {
Chatbots::Chatbots(not_null<Session*> 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<ChatbotsSettings> Chatbots::value() const {
return _settings.value();
}
void Chatbots::save(ChatbotsSettings settings, Fn<void(QString)> fail) {
void Chatbots::save(
ChatbotsSettings settings,
Fn<void()> done,
Fn<void(QString)> 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;
}

View file

@ -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*> session);
explicit Chatbots(not_null<Session*> owner);
~Chatbots();
void preload();
[[nodiscard]] const ChatbotsSettings &current() const;
[[nodiscard]] rpl::producer<ChatbotsSettings> changes() const;
[[nodiscard]] rpl::producer<ChatbotsSettings> value() const;
void save(ChatbotsSettings settings, Fn<void(QString)> fail);
void save(
ChatbotsSettings settings,
Fn<void()> done,
Fn<void(QString)> fail);
private:
const not_null<Session*> _session;
const not_null<Session*> _owner;
rpl::variable<ChatbotsSettings> _settings;
mtpRequestId _requestId = 0;
bool _loaded = false;
};

View file

@ -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<Session*> 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<MTPBusinessWorkHours> &hours,
const tl::conditional<MTPBusinessLocation> &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<Session*> owner,
const tl::conditional<MTPBusinessAwayMessage> &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<Session*> owner,
const tl::conditional<MTPBusinessGreetingMessage> &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)));
}

View file

@ -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<Session*> 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<MTPBusinessWorkHours> &hours,
const tl::conditional<MTPBusinessLocation> &location);
enum class AwayScheduleType : uchar {
Never = 0,
Always = 1,
@ -204,6 +216,10 @@ struct AwaySettings {
const AwaySettings &b) = default;
};
[[nodiscard]] AwaySettings FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessAwayMessage> &message);
struct GreetingSettings {
BusinessRecipients recipients;
int noActivityDays = 0;
@ -218,4 +234,8 @@ struct GreetingSettings {
const GreetingSettings &b) = default;
};
[[nodiscard]] GreetingSettings FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessGreetingMessage> &message);
} // namespace Data

View file

@ -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 <typename Flag>
[[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);

View file

@ -32,102 +32,6 @@ constexpr auto kSetOnlineAfterActivity = TimeId(30);
using UpdateFlag = Data::PeerUpdate::Flag;
[[nodiscard]] Data::BusinessDetails FromMTP(
const tl::conditional<MTPBusinessWorkHours> &hours,
const tl::conditional<MTPBusinessLocation> &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<Data::Session*> 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<Data::Session*> owner,
const tl::conditional<MTPBusinessAwayMessage> &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<Data::Session*> owner,
const tl::conditional<MTPBusinessGreetingMessage> &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;

View file

@ -211,12 +211,14 @@ base::options::toggle ShowPeerIdBelowAbout({
[[nodiscard]] rpl::producer<QString> OpensInText(
rpl::producer<TimeId> in,
rpl::producer<bool> hoursExpanded,
rpl::producer<QString> 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(

View file

@ -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);
}

View file

@ -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<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 rpl::single(field->getLastText());
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();
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) {
return rpl::never<BotState>();
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()) {
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<mtpRequestId>();
*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<Ui::RpWidget> MakeBotPreview(
not_null<QWidget*> parent,
not_null<Ui::RpWidget*> parent,
rpl::producer<BotState> state,
Fn<void()> resetBot) {
return object_ptr<Ui::RpWidget>(parent.get());
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, 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) {
const auto content = Ui::CreateChild<Ui::RpWidget>(inner);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
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); });
}

View file

@ -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);
}

View file

@ -631,4 +631,10 @@ settingsAddReplyField: InputField(defaultInputField) {
placeholderScale: 0.;
heightMin: 36px;
}
}
settingsChatbotsNotFound: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
align: align(top);
}
settingsChatbotsDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }};
settingsChatbotsDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }};

View file

@ -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<Order>(
const auto mtpOrder = FallbackOrder(); AssertIsDebug();/* account.appConfig().get<Order>(
"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<Ui::VerticalLayout>(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);