mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-18 07:07:08 +02:00
Implement preview and save of chatbots.
This commit is contained in:
parent
ea36345eee
commit
e3f6c189a7
13 changed files with 536 additions and 176 deletions
Telegram
Resources/langs
SourceFiles
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ¤t() 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;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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); });
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 }};
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue