Support business bot state in chat.

This commit is contained in:
John Preston 2024-03-18 15:02:12 +04:00
parent 3d97ea6f96
commit cf1d0677d1
20 changed files with 588 additions and 52 deletions

View File

@ -2288,6 +2288,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_chatbots_not_found" = "Chatbot not found.";
"lng_chatbots_add" = "Add";
"lng_chatbots_info_url" = "https://telegram.org/privacy";
"lng_chatbot_status_can_reply" = "bot manages this chat";
"lng_chatbot_status_paused" = "bot stopped";
"lng_chatbot_status_views" = "bot has access to this chat";
"lng_chatbot_button_pause" = "Stop";
"lng_chatbot_button_resume" = "Start";
"lng_chatbot_menu_manage" = "Manage bot";
"lng_chatbot_menu_remove" = "Remove bot from this chat";
"lng_chatbot_menu_revoke" = "Revoke access to this chat";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_group_button" = "Boost Group";

View File

@ -351,9 +351,9 @@ Main::Session &EditFilterChatsListController::session() const {
}
int EditFilterChatsListController::selectedTypesCount() const {
Expects(_chatlist || _typesDelegate != nullptr);
Expects(_chatlist || !_options || _typesDelegate != nullptr);
if (_chatlist) {
if (_chatlist || !_options) {
return 0;
}
auto result = 0;
@ -396,7 +396,7 @@ bool EditFilterChatsListController::handleDeselectForeignRow(
void EditFilterChatsListController::prepareViewHook() {
delegate()->peerListSetTitle(std::move(_title));
if (!_chatlist) {
if (!_chatlist && _options) {
delegate()->peerListSetAboveWidget(prepareTypesList());
}
@ -479,7 +479,8 @@ object_ptr<Ui::RpWidget> EditFilterChatsListController::prepareTypesList() {
auto EditFilterChatsListController::createRow(not_null<History*> history)
-> std::unique_ptr<Row> {
const auto business = _options & (Flag::NewChats | Flag::ExistingChats);
const auto business = (_options & (Flag::NewChats | Flag::ExistingChats))
|| (!_options && !_chatlist);
if (business && (history->peer->isSelf() || !history->peer->isUser())) {
return nullptr;
}

View File

@ -857,6 +857,31 @@ historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) {
}
historyContactStatusMinSkip: 16px;
historyBusinessBotPhoto: UserpicButton(defaultUserpicButton) {
size: size(46px, 46px);
photoSize: 46px;
photoPosition: point(0px, 0px);
}
historyBusinessBotName: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
}
historyBusinessBotStatus: FlatLabel(historyContactStatusLabel) {
textFg: windowSubTextFg;
}
historyBusinessBotToggle: defaultActiveButton;
historyBusinessBotSettings: IconButton(defaultIconButton) {
icon: icon{{ "menu/customize", menuIconFg }};
iconOver: icon{{ "menu/customize", menuIconFgOver }};
iconPosition: point(-1px, -1px);
rippleAreaSize: 40px;
rippleAreaPosition: point(4px, 9px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
height: 58px;
width: 48px;
}
historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }};
historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }};

View File

@ -87,7 +87,7 @@ void Chatbots::save(
? Flag::f_can_reply
: Flag()),
(settings.bot ? settings.bot : was.bot)->inputUser,
ToMTP(settings.recipients)
ForBotsToMTP(settings.recipients)
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
if (done) {
@ -103,4 +103,72 @@ void Chatbots::save(
_settings = settings;
}
void Chatbots::togglePaused(not_null<PeerData*> peer, bool paused) {
const auto type = paused
? SentRequestType::Pause
: SentRequestType::Unpause;
const auto api = &_owner->session().api();
const auto i = _sentRequests.find(peer);
if (i != end(_sentRequests)) {
const auto already = i->second.type;
if (already == SentRequestType::Remove || already == type) {
return;
}
api->request(i->second.requestId).cancel();
_sentRequests.erase(i);
}
const auto id = api->request(MTPaccount_ToggleConnectedBotPaused(
peer->input,
MTP_bool(paused)
)).done([=] {
if (_sentRequests[peer].type != type) {
return;
} else if (const auto settings = peer->barSettings()) {
peer->setBarSettings(paused
? (*settings | PeerBarSetting::BusinessBotPaused)
: (*settings & ~PeerBarSetting::BusinessBotPaused));
} else {
api->requestPeerSettings(peer);
}
_sentRequests.remove(peer);
}).fail([=] {
if (_sentRequests[peer].type != type) {
return;
}
api->requestPeerSettings(peer);
_sentRequests.remove(peer);
}).send();
_sentRequests[peer] = SentRequest{ type, id };
}
void Chatbots::removeFrom(not_null<PeerData*> peer) {
const auto type = SentRequestType::Remove;
const auto api = &_owner->session().api();
const auto i = _sentRequests.find(peer);
if (i != end(_sentRequests)) {
const auto already = i->second.type;
if (already == type) {
return;
}
api->request(i->second.requestId).cancel();
_sentRequests.erase(i);
}
const auto id = api->request(MTPaccount_DisablePeerConnectedBot(
peer->input
)).done([=] {
if (_sentRequests[peer].type != type) {
return;
} else if (const auto settings = peer->barSettings()) {
peer->clearBusinessBot();
} else {
api->requestPeerSettings(peer);
}
_sentRequests.remove(peer);
}).fail([=] {
api->requestPeerSettings(peer);
_sentRequests.remove(peer);
}).send();
_sentRequests[peer] = SentRequest{ type, id };
}
} // namespace Data

View File

@ -41,13 +41,28 @@ public:
Fn<void()> done,
Fn<void(QString)> fail);
void togglePaused(not_null<PeerData*> peer, bool paused);
void removeFrom(not_null<PeerData*> peer);
private:
enum class SentRequestType {
Pause,
Unpause,
Remove,
};
struct SentRequest {
SentRequestType type = SentRequestType::Pause;
mtpRequestId requestId = 0;
};
const not_null<Session*> _owner;
rpl::variable<ChatbotsSettings> _settings;
mtpRequestId _requestId = 0;
bool _loaded = false;
base::flat_map<not_null<PeerData*>, SentRequest> _sentRequests;
};
} // namespace Data

View File

@ -51,16 +51,13 @@ constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax;
return intervals;
}
} // namespace
MTPInputBusinessRecipients ToMTP(
const BusinessRecipients &data) {
using Flag = MTPDinputBusinessRecipients::Flag;
using Type = BusinessChatType;
template <typename Flag>
auto RecipientsFlags(const BusinessRecipients &data) {
const auto &chats = data.allButExcluded
? data.excluded
: data.included;
const auto flags = Flag()
using Type = BusinessChatType;
return Flag()
| ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag())
| ((chats.types & Type::ExistingChats)
? Flag::f_existing_chats
@ -69,12 +66,30 @@ MTPInputBusinessRecipients ToMTP(
| ((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;
}
} // namespace
MTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) {
using Flag = MTPDinputBusinessRecipients::Flag;
const auto &chats = data.allButExcluded ? data.excluded : data.included;
return MTP_inputBusinessRecipients(
MTP_flags(flags),
MTP_vector_from_range(users.list
MTP_flags(RecipientsFlags<Flag>(data)),
MTP_vector_from_range(chats.list
| ranges::views::transform(&UserData::inputUser)));
}
MTPInputBusinessBotRecipients ForBotsToMTP(const BusinessRecipients &data) {
using Flag = MTPDinputBusinessBotRecipients::Flag;
const auto &chats = data.allButExcluded ? data.excluded : data.included;
return MTP_inputBusinessBotRecipients(
MTP_flags(RecipientsFlags<Flag>(data)
| ((data.allButExcluded || data.excluded.empty())
? Flag()
: Flag::f_exclude_users)),
MTP_vector_from_range(chats.list
| ranges::views::transform(&UserData::inputUser)),
MTP_vector_from_range(data.excluded.list
| ranges::views::transform(&UserData::inputUser)));
}
@ -103,7 +118,40 @@ BusinessRecipients FromMTP(
return result;
}
[[nodiscard]] BusinessDetails FromMTP(
BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessBotRecipients &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)));
}
}
if (!result.allButExcluded) {
if (const auto excluded = data.vexclude_users()) {
for (const auto &userId : excluded->v) {
result.excluded.list.push_back(
owner->user(UserId(userId.v)));
}
}
}
return result;
}
BusinessDetails FromMTP(
const tl::conditional<MTPBusinessWorkHours> &hours,
const tl::conditional<MTPBusinessLocation> &location) {
auto result = BusinessDetails();

View File

@ -49,11 +49,21 @@ struct BusinessRecipients {
const BusinessRecipients &b) = default;
};
[[nodiscard]] MTPInputBusinessRecipients ToMTP(
enum class BusinessRecipientsType : uchar {
Messages,
Bots,
};
[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP(
const BusinessRecipients &data);
[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP(
const BusinessRecipients &data);
[[nodiscard]] BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessRecipients &recipients);
[[nodiscard]] BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessBotRecipients &recipients);
struct Timezone {
QString id;

View File

@ -49,14 +49,14 @@ namespace {
MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()),
MTP_int(data.shortcutId),
ToMTP(data.schedule),
ToMTP(data.recipients));
ForMessagesToMTP(data.recipients));
}
[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP(
const GreetingSettings &data) {
return MTP_inputBusinessGreetingMessage(
MTP_int(data.shortcutId),
ToMTP(data.recipients),
ForMessagesToMTP(data.recipients),
MTP_int(data.noActivityDays));
}

View File

@ -608,6 +608,23 @@ void PeerData::checkFolder(FolderId folderId) {
}
}
void PeerData::clearBusinessBot() {
if (const auto details = _barDetails.get()) {
if (details->requestChatDate) {
details->businessBot = nullptr;
details->businessBotManageUrl = QString();
} else {
_barDetails = nullptr;
}
}
if (const auto settings = barSettings()) {
setBarSettings(*settings
& ~PeerBarSetting::BusinessBotPaused
& ~PeerBarSetting::BusinessBotCanReply
& ~PeerBarSetting::HasBusinessBot);
}
}
void PeerData::setTranslationDisabled(bool disabled) {
const auto flag = disabled
? TranslationFlag::Disabled

View File

@ -373,6 +373,7 @@ public:
[[nodiscard]] TimeId requestChatDate() const;
[[nodiscard]] UserData *businessBot() const;
[[nodiscard]] QString businessBotManageUrl() const;
void clearBusinessBot();
enum class TranslationFlag : uchar {
Unknown,

View File

@ -1578,6 +1578,9 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) {
void HistoryWidget::orderWidgets() {
_voiceRecordBar->raise();
_send->raise();
if (_businessBotStatus) {
_businessBotStatus->bar().raise();
}
if (_contactStatus) {
_contactStatus->bar().raise();
}
@ -2282,17 +2285,30 @@ void HistoryWidget::showHistory(
_showAtMsgHighlightPartOffsetHint = highlightPartOffsetHint;
_historyInited = false;
_contactStatus = nullptr;
_businessBotStatus = nullptr;
if (peerId) {
using namespace HistoryView;
_peer = session().data().peer(peerId);
_contactStatus = std::make_unique<HistoryView::ContactStatus>(
_contactStatus = std::make_unique<ContactStatus>(
controller(),
this,
_peer,
false);
_contactStatus->bar().heightValue() | rpl::start_with_next([=] {
_contactStatus->bar().heightValue(
) | rpl::start_with_next([=] {
updateControlsGeometry();
}, _contactStatus->bar().lifetime());
if (const auto user = _peer->asUser()) {
_businessBotStatus = std::make_unique<BusinessBotStatus>(
controller(),
this,
user);
_businessBotStatus->bar().heightValue(
) | rpl::start_with_next([=] {
updateControlsGeometry();
}, _businessBotStatus->bar().lifetime());
}
orderWidgets();
controller()->tabbedSelector()->setCurrentPeer(_peer);
}
@ -2879,6 +2895,9 @@ void HistoryWidget::updateControlsVisibility() {
if (_contactStatus) {
_contactStatus->show();
}
if (_businessBotStatus) {
_businessBotStatus->show();
}
if (isChoosingTheme()
|| (!editingMessage()
&& (isSearching()
@ -4059,6 +4078,9 @@ void HistoryWidget::hideChildWidgets() {
if (_contactStatus) {
_contactStatus->hide();
}
if (_businessBotStatus) {
_businessBotStatus->hide();
}
hideChildren();
}
@ -5840,7 +5862,11 @@ void HistoryWidget::updateControlsGeometry() {
if (_contactStatus) {
_contactStatus->bar().move(0, contactStatusTop);
}
const auto scrollAreaTop = contactStatusTop + (_contactStatus ? _contactStatus->bar().height() : 0);
const auto businessBotTop = contactStatusTop + (_contactStatus ? _contactStatus->bar().height() : 0);
if (_businessBotStatus) {
_businessBotStatus->bar().move(0, businessBotTop);
}
const auto scrollAreaTop = businessBotTop + (_businessBotStatus ? _businessBotStatus->bar().height() : 0);
if (_scroll->y() != scrollAreaTop) {
_scroll->moveToLeft(0, scrollAreaTop);
_fieldAutocomplete->setBoundings(_scroll->geometry());
@ -6076,6 +6102,9 @@ void HistoryWidget::updateHistoryGeometry(
if (_contactStatus) {
newScrollHeight -= _contactStatus->bar().height();
}
if (_businessBotStatus) {
newScrollHeight -= _businessBotStatus->bar().height();
}
if (isChoosingTheme()) {
newScrollHeight -= _chooseTheme->height();
} else if (!editingMessage()
@ -6457,6 +6486,7 @@ int HistoryWidget::computeMaxFieldHeight() const {
const auto available = height()
- _topBar->height()
- (_contactStatus ? _contactStatus->bar().height() : 0)
- (_businessBotStatus ? _businessBotStatus->bar().height() : 0)
- (_pinnedBar ? _pinnedBar->height() : 0)
- (_groupCallBar ? _groupCallBar->height() : 0)
- (_requestsBar ? _requestsBar->height() : 0)

View File

@ -89,6 +89,7 @@ namespace HistoryView {
class StickerToast;
class TopBarWidget;
class ContactStatus;
class BusinessBotStatus;
class Element;
class PinnedTracker;
class TranslateBar;
@ -744,6 +745,7 @@ private:
bool _isInlineBot = false;
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;
const std::shared_ptr<Ui::SendButton> _send;
object_ptr<Ui::FlatButton> _unblock;

View File

@ -8,9 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_contact_status.h"
#include "lang/lang_keys.h"
#include "ui/controls/userpic_button.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/layers/generic_box.h"
#include "ui/toast/toast.h"
@ -18,7 +21,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "core/click_handler_types.h"
#include "core/ui_integration.h"
#include "data/business/data_business_chatbots.h"
#include "data/notify/data_notify_settings.h"
#include "data/data_peer.h"
#include "data/data_user.h"
@ -839,6 +844,241 @@ void ContactStatus::hide() {
_bar.hide();
}
class BusinessBotStatus::Bar final : public Ui::RpWidget {
public:
Bar(QWidget *parent);
void showState(State state);
[[nodiscard]] rpl::producer<> pauseClicks() const;
[[nodiscard]] rpl::producer<> resumeClicks() const;
[[nodiscard]] rpl::producer<> removeClicks() const;
[[nodiscard]] rpl::producer<> manageClicks() const;
private:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
void showMenu();
object_ptr<Ui::UserpicButton> _userpic = { nullptr };
object_ptr<Ui::FlatLabel> _name;
object_ptr<Ui::FlatLabel> _status;
object_ptr<Ui::RoundButton> _togglePaused;
object_ptr<Ui::IconButton> _settings;
rpl::event_stream<> _removeClicks;
rpl::event_stream<> _manageClicks;
base::unique_qptr<Ui::PopupMenu> _menu;
bool _paused = false;
};
BusinessBotStatus::Bar::Bar(QWidget *parent)
: RpWidget(parent)
, _name(this, st::historyBusinessBotName)
, _status(this, st::historyBusinessBotStatus)
, _togglePaused(
this,
rpl::single(QString()),
st::historyBusinessBotToggle)
, _settings(this, st::historyBusinessBotSettings) {
_name->setAttribute(Qt::WA_TransparentForMouseEvents);
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
_settings->setClickedCallback([=] {
showMenu();
});
}
void BusinessBotStatus::Bar::showState(State state) {
Expects(state.bot != nullptr);
_userpic = object_ptr<Ui::UserpicButton>(
this,
state.bot,
st::historyBusinessBotPhoto);
_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
_userpic->show();
_name->setText(state.bot->name());
_status->setText(!state.canReply
? tr::lng_chatbot_status_views(tr::now)
: state.paused
? tr::lng_chatbot_status_paused(tr::now)
: tr::lng_chatbot_status_can_reply(tr::now));
_togglePaused->setText(state.paused
? tr::lng_chatbot_button_resume()
: tr::lng_chatbot_button_pause());
_togglePaused->setVisible(state.canReply);
_paused = state.paused;
resizeToWidth(width());
}
rpl::producer<> BusinessBotStatus::Bar::pauseClicks() const {
return _togglePaused->clicks() | rpl::filter([=] {
return !_paused;
}) | rpl::to_empty;
}
rpl::producer<> BusinessBotStatus::Bar::resumeClicks() const {
return _togglePaused->clicks() | rpl::filter([=] {
return _paused;
}) | rpl::to_empty;
}
rpl::producer<> BusinessBotStatus::Bar::removeClicks() const {
return _removeClicks.events();
}
rpl::producer<> BusinessBotStatus::Bar::manageClicks() const {
return _manageClicks.events();
}
void BusinessBotStatus::Bar::showMenu() {
if (_menu) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuExpandedSeparator);
_menu->setDestroyedCallback([
weak = Ui::MakeWeak(this),
weakButton = Ui::MakeWeak(_settings.data()),
menu = _menu.get()] {
if (weak && weak->_menu == menu) {
if (weakButton) {
weakButton->setForceRippled(false);
}
}
});
_settings->setForceRippled(true);
const auto addAction = Ui::Menu::CreateAddActionCallback(_menu);
addAction(tr::lng_chatbot_menu_manage(tr::now), crl::guard(this, [=] {
_manageClicks.fire({});
}), &st::menuIconSettings);
addAction({
.text = (_togglePaused->isHidden()
? tr::lng_chatbot_menu_revoke(tr::now)
: tr::lng_chatbot_menu_remove(tr::now)),
.handler = crl::guard(this, [=] { _removeClicks.fire({}); }),
.icon = &st::menuIconDisableAttention,
.isAttention = true,
});
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
_menu->popup(mapToGlobal(QPoint(
width() + st::topBarMenuPosition.x(),
st::topBarMenuPosition.y())));
}
void BusinessBotStatus::Bar::paintEvent(QPaintEvent *e) {
QPainter p(this);
p.fillRect(e->rect(), st::historyContactStatusButton.bgColor);
}
int BusinessBotStatus::Bar::resizeGetHeight(int newWidth) {
const auto &st = st::defaultPeerList.item;
_settings->moveToRight(0, 0, newWidth);
if (_userpic) {
_userpic->moveToLeft(st.photoPosition.x(), st.photoPosition.y());
}
auto available = newWidth - _settings->width() - st.namePosition.x();
if (!_togglePaused->isHidden()) {
_togglePaused->moveToRight(_settings->width(), 0);
available -= _togglePaused->width();
}
_name->resizeToWidth(available);
_name->moveToLeft(st.namePosition.x(), st.namePosition.y());
_status->resizeToWidth(available);
_status->moveToLeft(st.statusPosition.x(), st.statusPosition.y());
return st.height;
}
BusinessBotStatus::BusinessBotStatus(
not_null<Window::SessionController*> window,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer)
: _controller(window)
, _inner(Ui::CreateChild<Bar>(parent.get()))
, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) {
setupState(peer);
setupHandlers(peer);
}
auto BusinessBotStatus::PeerState(not_null<PeerData*> peer)
-> rpl::producer<State> {
using SettingsChange = PeerData::BarSettings::Change;
return peer->barSettingsValue(
) | rpl::map([=](SettingsChange settings) -> State {
using Flag = PeerBarSetting;
return {
.bot = peer->businessBot(),
.manageUrl = peer->businessBotManageUrl(),
.canReply = ((settings.value & Flag::BusinessBotCanReply) != 0),
.paused = ((settings.value & Flag::BusinessBotPaused) != 0),
};
});
}
void BusinessBotStatus::setupState(not_null<PeerData*> peer) {
if (!BarCurrentlyHidden(peer)) {
peer->session().api().requestPeerSettings(peer);
}
PeerState(
peer
) | rpl::start_with_next([=](State state) {
_state = state;
if (!state.bot) {
_bar.toggleContent(false);
} else {
_inner->showState(state);
_bar.toggleContent(true);
}
}, _bar.lifetime());
}
void BusinessBotStatus::setupHandlers(not_null<PeerData*> peer) {
_inner->pauseClicks(
) | rpl::start_with_next([=] {
peer->owner().chatbots().togglePaused(peer, true);
}, _bar.lifetime());
_inner->resumeClicks(
) | rpl::start_with_next([=] {
peer->owner().chatbots().togglePaused(peer, false);
}, _bar.lifetime());
_inner->removeClicks(
) | rpl::start_with_next([=] {
peer->owner().chatbots().removeFrom(peer);
}, _bar.lifetime());
_inner->manageClicks(
) | rpl::start_with_next([=] {
UrlClickHandler::Open(
_state.manageUrl,
QVariant::fromValue(ClickHandlerContext{
.sessionWindow = base::make_weak(_controller),
.botStartAutoSubmit = true,
}));
}, _bar.lifetime());
}
void BusinessBotStatus::show() {
if (!_shown) {
_shown = true;
if (_state.bot) {
_inner->showState(_state);
_bar.toggleContent(true);
}
}
_bar.show();
}
void BusinessBotStatus::hide() {
_bar.hide();
}
TopicReopenBar::TopicReopenBar(
not_null<Ui::RpWidget*> parent,
not_null<Data::ForumTopic*> topic)

View File

@ -124,6 +124,43 @@ private:
};
class BusinessBotStatus final {
public:
BusinessBotStatus(
not_null<Window::SessionController*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer);
void show();
void hide();
[[nodiscard]] SlidingBar &bar() {
return _bar;
}
private:
class Bar;
struct State {
UserData *bot = nullptr;
QString manageUrl;
bool canReply = false;
bool paused = false;
};
void setupState(not_null<PeerData*> peer);
void setupHandlers(not_null<PeerData*> peer);
static rpl::producer<State> PeerState(not_null<PeerData*> peer);
const not_null<Window::SessionController*> _controller;
State _state;
QPointer<Bar> _inner;
SlidingBar _bar;
bool _shown = false;
};
class TopicReopenBar final {
public:
TopicReopenBar(

View File

@ -1704,7 +1704,7 @@ inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut;
messages.quickReplies#c68d6695 quick_replies:Vector<QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies;
messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies;
connectedBot#e7e999e7 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessRecipients = ConnectedBot;
connectedBot#bd068601 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessBotRecipients = ConnectedBot;
account.connectedBots#17d7f87b connected_bots:Vector<ConnectedBot> users:Vector<User> = account.ConnectedBots;
@ -1723,6 +1723,10 @@ inputCollectiblePhone#a2e214a4 phone:string = InputCollectible;
fragment.collectibleInfo#6ebdff91 purchase_date:int currency:string amount:long crypto_currency:string crypto_amount:long url:string = fragment.CollectibleInfo;
inputBusinessBotRecipients#c4e5921e flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<InputUser> exclude_users:flags.6?Vector<InputUser> = InputBusinessBotRecipients;
businessBotRecipients#b88cf373 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<long> exclude_users:flags.6?Vector<long> = BusinessBotRecipients;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -1853,7 +1857,7 @@ account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?Bus
account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool;
account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool;
account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool;
account.updateConnectedBot#9c2d527d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessRecipients = Updates;
account.updateConnectedBot#43d8521d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessBotRecipients = Updates;
account.getConnectedBots#4ea4c80f = account.ConnectedBots;
account.getBotBusinessConnection#76a86270 connection_id:string = Updates;
account.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool;

View File

@ -336,6 +336,7 @@ void AwayMessage::setupContent(
.controller = controller,
.title = tr::lng_away_recipients(),
.data = &_recipients,
.type = Data::BusinessRecipientsType::Messages,
});
Ui::AddSkip(inner, st::settingsChatbotsAccessSkip);

View File

@ -448,6 +448,7 @@ void Chatbots::setupContent(
.controller = controller,
.title = tr::lng_chatbots_access_title(),
.data = &_recipients,
.type = Data::BusinessRecipientsType::Bots,
});
Ui::AddSkip(content, st::settingsChatbotsAccessSkip);

View File

@ -229,6 +229,7 @@ void Greeting::setupContent(
.controller = controller,
.title = tr::lng_greeting_recipients(),
.data = &_recipients,
.type = Data::BusinessRecipientsType::Messages,
});
Ui::AddSkip(inner);

View File

@ -69,7 +69,7 @@ void EditBusinessChats(
(descriptor.include
? tr::lng_filters_include_title()
: tr::lng_filters_exclude_title()),
options,
(descriptor.usersOnly ? Flag() : options),
TypesToFlags(descriptor.current.types) & options,
base::flat_set<not_null<History*>>(begin(peers), end(peers)),
100,
@ -162,6 +162,8 @@ void AddBusinessRecipientsSelector(
auto &lifetime = container->lifetime();
const auto controller = descriptor.controller;
const auto data = descriptor.data;
const auto includeWithExcluded = (descriptor.type
== Data::BusinessRecipientsType::Bots);
const auto change = [=](Fn<void(Data::BusinessRecipients&)> modify) {
auto now = data->current();
modify(now);
@ -191,11 +193,17 @@ void AddBusinessRecipientsSelector(
Ui::AddSkip(container, st::settingsChatbotsAccessSkip);
Ui::AddDivider(container);
const auto includeWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container))
)->setDuration(0);
const auto excludeWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container))
)->setDuration(0);
const auto excludeInner = excludeWrap->entity();
Ui::AddSkip(excludeInner);
@ -205,18 +213,34 @@ void AddBusinessRecipientsSelector(
tr::lng_chatbots_exclude_button(),
st::settingsChatbotsAdd,
{ &st::settingsIconRemove, IconType::Round, &st::windowBgActive });
excludeAdd->setClickedCallback([=] {
const auto addExcluded = [=] {
const auto save = [=](Data::BusinessChats value) {
change([&](Data::BusinessRecipients &data) {
if (includeWithExcluded) {
if (!data.allButExcluded) {
value.types = {};
}
for (const auto &user : value.list) {
data.included.list.erase(
ranges::remove(data.included.list, user),
end(data.included.list));
}
}
if (!value.empty()) {
data.included = {};
}
data.excluded = std::move(value);
});
};
EditBusinessChats(controller, {
.current = data->current().excluded,
.save = crl::guard(excludeAdd, save),
.usersOnly = (includeWithExcluded
&& !data->current().allButExcluded),
.include = false,
});
});
};
excludeAdd->setClickedCallback(addExcluded);
const auto excluded = lifetime.make_state<
rpl::variable<Data::BusinessChats>
@ -227,24 +251,19 @@ void AddBusinessRecipientsSelector(
}, lifetime);
excluded->changes(
) | rpl::start_with_next([=](Data::BusinessChats &&value) {
auto now = data->current();
now.excluded = std::move(value);
*data = std::move(now);
change([&](Data::BusinessRecipients &data) {
data.excluded = std::move(value);
});
}, lifetime);
SetupBusinessChatsPreview(excludeInner, excluded);
excludeWrap->toggleOn(data->value(
) | rpl::map([](const Data::BusinessRecipients &value) {
return value.allButExcluded;
) | rpl::map([=](const Data::BusinessRecipients &value) {
return value.allButExcluded || includeWithExcluded;
}));
excludeWrap->finishAnimating();
const auto includeWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container))
)->setDuration(0);
const auto includeInner = includeWrap->entity();
Ui::AddSkip(includeInner);
@ -254,18 +273,32 @@ void AddBusinessRecipientsSelector(
tr::lng_chatbots_include_button(),
st::settingsChatbotsAdd,
{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });
includeAdd->setClickedCallback([=] {
const auto addIncluded = [=] {
const auto save = [=](Data::BusinessChats value) {
change([&](Data::BusinessRecipients &data) {
if (includeWithExcluded) {
for (const auto &user : value.list) {
data.excluded.list.erase(
ranges::remove(data.excluded.list, user),
end(data.excluded.list));
}
}
if (!value.empty()) {
data.excluded.types = {};
}
data.included = std::move(value);
});
if (!data->current().included.empty()) {
group->setValue(kSelectedOnly);
}
};
EditBusinessChats(controller, {
.current = data->current().included ,
.current = data->current().included,
.save = crl::guard(includeAdd, save),
.include = true,
});
});
};
includeAdd->setClickedCallback(addIncluded);
const auto included = lifetime.make_state<
rpl::variable<Data::BusinessChats>
@ -298,16 +331,7 @@ void AddBusinessRecipientsSelector(
group->setChangedCallback([=](int value) {
if (value == kSelectedOnly && data->current().included.empty()) {
group->setValue(kAllExcept);
const auto save = [=](Data::BusinessChats value) {
change([&](Data::BusinessRecipients &data) {
data.included = std::move(value);
});
group->setValue(kSelectedOnly);
};
EditBusinessChats(controller, {
.save = crl::guard(includeAdd, save),
.include = true,
});
addIncluded();
return;
}
change([&](Data::BusinessRecipients &data) {

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/required.h"
#include "data/business/data_business_common.h"
#include "settings/settings_common_session.h"
@ -52,6 +53,7 @@ private:
struct BusinessChatsDescriptor {
Data::BusinessChats current;
Fn<void(const Data::BusinessChats&)> save;
bool usersOnly = false;
bool include = false;
};
void EditBusinessChats(
@ -66,6 +68,7 @@ struct BusinessRecipientsSelectorDescriptor {
not_null<Window::SessionController*> controller;
rpl::producer<QString> title;
not_null<rpl::variable<Data::BusinessRecipients>*> data;
base::required<Data::BusinessRecipientsType> type;
};
void AddBusinessRecipientsSelector(
not_null<Ui::VerticalLayout*> container,