Implement chatbots section editing.

This commit is contained in:
John Preston 2024-02-20 16:02:02 +04:00
parent 205479fccc
commit ad9107ca90
18 changed files with 670 additions and 234 deletions

View File

@ -180,6 +180,8 @@ PRIVATE
boxes/filters/edit_filter_box.h
boxes/filters/edit_filter_chats_list.cpp
boxes/filters/edit_filter_chats_list.h
boxes/filters/edit_filter_chats_preview.cpp
boxes/filters/edit_filter_chats_preview.h
boxes/filters/edit_filter_links.cpp
boxes/filters/edit_filter_links.h
boxes/peers/add_bot_to_chat_box.cpp
@ -446,6 +448,9 @@ PRIVATE
core/version.h
countries/countries_manager.cpp
countries/countries_manager.h
data/business/data_business_chatbots.cpp
data/business/data_business_chatbots.h
data/business/data_business_common.h
data/notify/data_notify_settings.cpp
data/notify/data_notify_settings.h
data/notify/data_peer_notify_settings.cpp
@ -1277,6 +1282,8 @@ PRIVATE
profile/profile_block_widget.h
profile/profile_cover_drop_area.cpp
profile/profile_cover_drop_area.h
settings/business/settings_business_exceptions.cpp
settings/business/settings_business_exceptions.h
settings/business/settings_chatbots.cpp
settings/business/settings_chatbots.h
settings/cloud_password/settings_cloud_password_common.cpp

View File

@ -2189,6 +2189,8 @@ 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_add" = "Add";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_group_button" = "Boost Group";
@ -4341,6 +4343,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_filters_type_non_contacts" = "Non-Contacts";
"lng_filters_type_groups" = "Groups";
"lng_filters_type_channels" = "Channels";
"lng_filters_type_new" = "New Chats";
"lng_filters_type_existing" = "Existing Chats";
"lng_filters_type_bots" = "Bots";
"lng_filters_type_no_archived" = "Archived";
"lng_filters_type_no_muted" = "Muted";

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/filters/edit_filter_box.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "boxes/filters/edit_filter_chats_preview.h"
#include "boxes/filters/edit_filter_links.h"
#include "boxes/premium_limits_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
@ -56,60 +57,6 @@ using Flags = Data::ChatFilter::Flags;
using ExceptionPeersRef = const base::flat_set<not_null<History*>> &;
using ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const;
constexpr auto kAllTypes = {
Flag::Contacts,
Flag::NonContacts,
Flag::Groups,
Flag::Channels,
Flag::Bots,
Flag::NoMuted,
Flag::NoRead,
Flag::NoArchived,
};
class FilterChatsPreview final : public Ui::RpWidget {
public:
FilterChatsPreview(
not_null<QWidget*> parent,
Flags flags,
const base::flat_set<not_null<History*>> &peers);
[[nodiscard]] rpl::producer<Flag> flagRemoved() const;
[[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const;
void updateData(
Flags flags,
const base::flat_set<not_null<History*>> &peers);
int resizeGetHeight(int newWidth) override;
private:
using Button = base::unique_qptr<Ui::IconButton>;
struct FlagButton {
Flag flag = Flag();
Button button;
};
struct PeerButton {
not_null<History*> history;
Ui::PeerUserpicView userpic;
Ui::Text::String name;
Button button;
};
void paintEvent(QPaintEvent *e) override;
void refresh();
void removeFlag(Flag flag);
void removePeer(not_null<History*> history);
std::vector<FlagButton> _removeFlag;
std::vector<PeerButton> _removePeer;
rpl::event_stream<Flag> _flagRemoved;
rpl::event_stream<not_null<History*>> _peerRemoved;
};
struct NameEditing {
not_null<Ui::InputField*> field;
bool custom = false;
@ -167,167 +114,6 @@ not_null<FilterChatsPreview*> SetupChatsPreview(
return preview;
}
FilterChatsPreview::FilterChatsPreview(
not_null<QWidget*> parent,
Flags flags,
const base::flat_set<not_null<History*>> &peers)
: RpWidget(parent) {
updateData(flags, peers);
}
void FilterChatsPreview::refresh() {
resizeToWidth(width());
}
void FilterChatsPreview::updateData(
Flags flags,
const base::flat_set<not_null<History*>> &peers) {
_removeFlag.clear();
_removePeer.clear();
const auto makeButton = [&](Fn<void()> handler) {
auto result = base::make_unique_q<Ui::IconButton>(
this,
st::windowFilterSmallRemove);
result->setClickedCallback(std::move(handler));
return result;
};
for (const auto flag : kAllTypes) {
if (flags & flag) {
_removeFlag.push_back({
flag,
makeButton([=] { removeFlag(flag); }) });
}
}
for (const auto &history : peers) {
_removePeer.push_back(PeerButton{
.history = history,
.button = makeButton([=] { removePeer(history); })
});
}
refresh();
}
int FilterChatsPreview::resizeGetHeight(int newWidth) {
const auto right = st::windowFilterSmallRemoveRight;
const auto add = (st::windowFilterSmallItem.height
- st::windowFilterSmallRemove.height) / 2;
auto top = 0;
const auto moveNextButton = [&](not_null<Ui::IconButton*> button) {
button->moveToRight(right, top + add, newWidth);
top += st::windowFilterSmallItem.height;
};
for (const auto &[flag, button] : _removeFlag) {
moveNextButton(button.get());
}
for (const auto &[history, userpic, name, button] : _removePeer) {
moveNextButton(button.get());
}
return top;
}
void FilterChatsPreview::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
auto top = 0;
const auto &st = st::windowFilterSmallItem;
const auto iconLeft = st.photoPosition.x();
const auto iconTop = st.photoPosition.y();
const auto nameLeft = st.namePosition.x();
p.setFont(st::windowFilterSmallItem.nameStyle.font);
const auto nameTop = st.namePosition.y();
for (const auto &[flag, button] : _removeFlag) {
PaintFilterChatsTypeIcon(
p,
flag,
iconLeft,
top + iconTop,
width(),
st.photoSize);
p.setPen(st::contactsNameFg);
p.drawTextLeft(
nameLeft,
top + nameTop,
width(),
FilterChatsTypeName(flag));
top += st.height;
}
for (auto &[history, userpic, name, button] : _removePeer) {
const auto savedMessages = history->peer->isSelf();
const auto repliesMessages = history->peer->isRepliesChat();
if (savedMessages || repliesMessages) {
if (savedMessages) {
Ui::EmptyUserpic::PaintSavedMessages(
p,
iconLeft,
top + iconTop,
width(),
st.photoSize);
} else {
Ui::EmptyUserpic::PaintRepliesMessages(
p,
iconLeft,
top + iconTop,
width(),
st.photoSize);
}
p.setPen(st::contactsNameFg);
p.drawTextLeft(
nameLeft,
top + nameTop,
width(),
(savedMessages
? tr::lng_saved_messages(tr::now)
: tr::lng_replies_messages(tr::now)));
} else {
history->peer->paintUserpicLeft(
p,
userpic,
iconLeft,
top + iconTop,
width(),
st.photoSize);
p.setPen(st::contactsNameFg);
if (name.isEmpty()) {
name.setText(
st::msgNameStyle,
history->peer->name(),
Ui::NameTextOptions());
}
name.drawLeftElided(
p,
nameLeft,
top + nameTop,
button->x() - nameLeft,
width());
}
top += st.height;
}
}
void FilterChatsPreview::removeFlag(Flag flag) {
const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag);
Assert(i != end(_removeFlag));
_removeFlag.erase(i);
refresh();
_flagRemoved.fire_copy(flag);
}
void FilterChatsPreview::removePeer(not_null<History*> history) {
const auto i = ranges::find(_removePeer, history, &PeerButton::history);
Assert(i != end(_removePeer));
_removePeer.erase(i);
refresh();
_peerRemoved.fire_copy(history);
}
rpl::producer<Flag> FilterChatsPreview::flagRemoved() const {
return _flagRemoved.events();
}
rpl::producer<not_null<History*>> FilterChatsPreview::peerRemoved() const {
return _peerRemoved.events();
}
void EditExceptions(
not_null<Window::SessionController*> window,
not_null<QObject*> context,

View File

@ -28,6 +28,8 @@ using Flag = Data::ChatFilter::Flag;
using Flags = Data::ChatFilter::Flags;
constexpr auto kAllTypes = {
Flag::NewChats,
Flag::ExistingChats,
Flag::Contacts,
Flag::NonContacts,
Flag::Groups,
@ -119,7 +121,7 @@ PaintRoundImageCallback TypeRow::generatePaintUserpicCallback(
}
Flag TypeRow::flag() const {
return static_cast<Flag>(id() & 0xFF);
return static_cast<Flag>(id() & 0xFFFF);
}
ExceptionRow::ExceptionRow(not_null<History*> history) : Row(history) {
@ -219,6 +221,8 @@ auto TypeController::rowSelectionChanges() const
[[nodiscard]] QString FilterChatsTypeName(Flag flag) {
switch (flag) {
case Flag::NewChats: return tr::lng_filters_type_new(tr::now);
case Flag::ExistingChats: return tr::lng_filters_type_existing(tr::now);
case Flag::Contacts: return tr::lng_filters_type_contacts(tr::now);
case Flag::NonContacts:
return tr::lng_filters_type_non_contacts(tr::now);
@ -241,6 +245,8 @@ void PaintFilterChatsTypeIcon(
int size) {
const auto &color1 = [&]() -> const style::color& {
switch (flag) {
case Flag::NewChats: return st::historyPeer5UserpicBg;
case Flag::ExistingChats: return st::historyPeer8UserpicBg;
case Flag::Contacts: return st::historyPeer4UserpicBg;
case Flag::NonContacts: return st::historyPeer7UserpicBg;
case Flag::Groups: return st::historyPeer2UserpicBg;
@ -254,6 +260,8 @@ void PaintFilterChatsTypeIcon(
}();
const auto &color2 = [&]() -> const style::color& {
switch (flag) {
case Flag::NewChats: return st::historyPeer5UserpicBg2;
case Flag::ExistingChats: return st::historyPeer8UserpicBg2;
case Flag::Contacts: return st::historyPeer4UserpicBg2;
case Flag::NonContacts: return st::historyPeer7UserpicBg2;
case Flag::Groups: return st::historyPeer2UserpicBg2;
@ -267,6 +275,8 @@ void PaintFilterChatsTypeIcon(
}();
const auto &icon = [&]() -> const style::icon& {
switch (flag) {
case Flag::NewChats: return st::windowFilterTypeNewChats;
case Flag::ExistingChats: return st::windowFilterTypeExistingChats;
case Flag::Contacts: return st::windowFilterTypeContacts;
case Flag::NonContacts: return st::windowFilterTypeNonContacts;
case Flag::Groups: return st::windowFilterTypeGroups;
@ -469,6 +479,10 @@ 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);
if (business && (history->peer->isSelf() || !history->peer->isUser())) {
return nullptr;
}
return history->inChatList()
? std::make_unique<ExceptionRow>(history)
: nullptr;

View File

@ -0,0 +1,199 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/filters/edit_filter_chats_preview.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "data/data_peer.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "ui/text/text_options.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
#include "styles/style_window.h"
namespace {
using Flag = Data::ChatFilter::Flag;
constexpr auto kAllTypes = {
Flag::NewChats,
Flag::ExistingChats,
Flag::Contacts,
Flag::NonContacts,
Flag::Groups,
Flag::Channels,
Flag::Bots,
Flag::NoMuted,
Flag::NoRead,
Flag::NoArchived,
};
} // namespace
FilterChatsPreview::FilterChatsPreview(
not_null<QWidget*> parent,
Flags flags,
const base::flat_set<not_null<History*>> &peers)
: RpWidget(parent) {
updateData(flags, peers);
}
void FilterChatsPreview::refresh() {
resizeToWidth(width());
}
void FilterChatsPreview::updateData(
Flags flags,
const base::flat_set<not_null<History*>> &peers) {
_removeFlag.clear();
_removePeer.clear();
const auto makeButton = [&](Fn<void()> handler) {
auto result = base::make_unique_q<Ui::IconButton>(
this,
st::windowFilterSmallRemove);
result->setClickedCallback(std::move(handler));
result->show();
return result;
};
for (const auto flag : kAllTypes) {
if (flags & flag) {
_removeFlag.push_back({
flag,
makeButton([=] { removeFlag(flag); }) });
}
}
for (const auto &history : peers) {
_removePeer.push_back(PeerButton{
.history = history,
.button = makeButton([=] { removePeer(history); })
});
}
refresh();
}
int FilterChatsPreview::resizeGetHeight(int newWidth) {
const auto right = st::windowFilterSmallRemoveRight;
const auto add = (st::windowFilterSmallItem.height
- st::windowFilterSmallRemove.height) / 2;
auto top = 0;
const auto moveNextButton = [&](not_null<Ui::IconButton*> button) {
button->moveToRight(right, top + add, newWidth);
top += st::windowFilterSmallItem.height;
};
for (const auto &[flag, button] : _removeFlag) {
moveNextButton(button.get());
}
for (const auto &[history, userpic, name, button] : _removePeer) {
moveNextButton(button.get());
}
return top;
}
void FilterChatsPreview::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
auto top = 0;
const auto &st = st::windowFilterSmallItem;
const auto iconLeft = st.photoPosition.x();
const auto iconTop = st.photoPosition.y();
const auto nameLeft = st.namePosition.x();
p.setFont(st::windowFilterSmallItem.nameStyle.font);
const auto nameTop = st.namePosition.y();
for (const auto &[flag, button] : _removeFlag) {
PaintFilterChatsTypeIcon(
p,
flag,
iconLeft,
top + iconTop,
width(),
st.photoSize);
p.setPen(st::contactsNameFg);
p.drawTextLeft(
nameLeft,
top + nameTop,
width(),
FilterChatsTypeName(flag));
top += st.height;
}
for (auto &[history, userpic, name, button] : _removePeer) {
const auto savedMessages = history->peer->isSelf();
const auto repliesMessages = history->peer->isRepliesChat();
if (savedMessages || repliesMessages) {
if (savedMessages) {
Ui::EmptyUserpic::PaintSavedMessages(
p,
iconLeft,
top + iconTop,
width(),
st.photoSize);
} else {
Ui::EmptyUserpic::PaintRepliesMessages(
p,
iconLeft,
top + iconTop,
width(),
st.photoSize);
}
p.setPen(st::contactsNameFg);
p.drawTextLeft(
nameLeft,
top + nameTop,
width(),
(savedMessages
? tr::lng_saved_messages(tr::now)
: tr::lng_replies_messages(tr::now)));
} else {
history->peer->paintUserpicLeft(
p,
userpic,
iconLeft,
top + iconTop,
width(),
st.photoSize);
p.setPen(st::contactsNameFg);
if (name.isEmpty()) {
name.setText(
st::msgNameStyle,
history->peer->name(),
Ui::NameTextOptions());
}
name.drawLeftElided(
p,
nameLeft,
top + nameTop,
button->x() - nameLeft,
width());
}
top += st.height;
}
}
void FilterChatsPreview::removeFlag(Flag flag) {
const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag);
Assert(i != end(_removeFlag));
_removeFlag.erase(i);
refresh();
_flagRemoved.fire_copy(flag);
}
void FilterChatsPreview::removePeer(not_null<History*> history) {
const auto i = ranges::find(_removePeer, history, &PeerButton::history);
Assert(i != end(_removePeer));
_removePeer.erase(i);
refresh();
_peerRemoved.fire_copy(history);
}
rpl::producer<Flag> FilterChatsPreview::flagRemoved() const {
return _flagRemoved.events();
}
rpl::producer<not_null<History*>> FilterChatsPreview::peerRemoved() const {
return _peerRemoved.events();
}

View File

@ -0,0 +1,64 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/data_chat_filters.h"
#include "ui/rp_widget.h"
#include "ui/userpic_view.h"
class History;
namespace Ui {
class IconButton;
} // namespace Ui
class FilterChatsPreview final : public Ui::RpWidget {
public:
using Flag = Data::ChatFilter::Flag;
using Flags = Data::ChatFilter::Flags;
FilterChatsPreview(
not_null<QWidget*> parent,
Flags flags,
const base::flat_set<not_null<History*>> &peers);
[[nodiscard]] rpl::producer<Flag> flagRemoved() const;
[[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const;
void updateData(
Flags flags,
const base::flat_set<not_null<History*>> &peers);
int resizeGetHeight(int newWidth) override;
private:
using Button = base::unique_qptr<Ui::IconButton>;
struct FlagButton {
Flag flag = Flag();
Button button;
};
struct PeerButton {
not_null<History*> history;
Ui::PeerUserpicView userpic;
Ui::Text::String name;
Button button;
};
void paintEvent(QPaintEvent *e) override;
void refresh();
void removeFlag(Flag flag);
void removePeer(not_null<History*> history);
std::vector<FlagButton> _removeFlag;
std::vector<PeerButton> _removePeer;
rpl::event_stream<Flag> _flagRemoved;
rpl::event_stream<not_null<History*>> _peerRemoved;
};

View File

@ -982,8 +982,7 @@ bool GoodForExportFilterLink(
not_null<Window::SessionController*> window,
const Data::ChatFilter &filter) {
using Flag = Data::ChatFilter::Flag;
const auto listflags = Flag::Chatlist | Flag::HasMyLinks;
if (!filter.never().empty() || (filter.flags() & ~listflags)) {
if (!filter.never().empty() || (filter.flags() & Flag::RulesMask)) {
window->showToast(tr::lng_filters_link_cant(tr::now));
return false;
}

View File

@ -0,0 +1,34 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/business/data_business_chatbots.h"
namespace Data {
Chatbots::Chatbots(not_null<Session*> session)
: _session(session) {
}
Chatbots::~Chatbots() = default;
const ChatbotsSettings &Chatbots::current() const {
return _settings.current();
}
rpl::producer<ChatbotsSettings> Chatbots::changes() const {
return _settings.changes();
}
rpl::producer<ChatbotsSettings> Chatbots::value() const {
return _settings.value();
}
void Chatbots::save(ChatbotsSettings settings) {
_settings = settings;
}
} // namespace Data

View File

@ -0,0 +1,44 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/business/data_business_common.h"
class UserData;
namespace Data {
class Session;
struct ChatbotsSettings {
UserData *bot = nullptr;
BusinessExceptions allowed;
BusinessExceptions disallowed;
bool repliesAllowed = false;
bool onlySelected = false;
};
class Chatbots final {
public:
explicit Chatbots(not_null<Session*> session);
~Chatbots();
[[nodiscard]] const ChatbotsSettings &current() const;
[[nodiscard]] rpl::producer<ChatbotsSettings> changes() const;
[[nodiscard]] rpl::producer<ChatbotsSettings> value() const;
void save(ChatbotsSettings settings);
private:
const not_null<Session*> _session;
rpl::variable<ChatbotsSettings> _settings;
};
} // namespace Data

View File

@ -0,0 +1,31 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/flags.h"
class UserData;
namespace Data {
enum class BusinessChatType {
NewChats = (1 << 0),
ExistingChats = (1 << 1),
Contacts = (1 << 2),
NonContacts = (1 << 3),
};
inline constexpr bool is_flag_type(BusinessChatType) { return true; }
using BusinessChatTypes = base::flags<BusinessChatType>;
struct BusinessExceptions {
BusinessChatTypes types;
std::vector<not_null<UserData*>> list;
};
} // namespace Data

View File

@ -163,6 +163,7 @@ ChatFilter ChatFilter::withTitle(const QString &title) const {
ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const {
auto result = *this;
result._flags &= Flag::RulesMask;
if (chatlist) {
result._flags |= Flag::Chatlist;
if (hasMyLinks) {
@ -170,8 +171,6 @@ ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const {
} else {
result._flags &= ~Flag::HasMyLinks;
}
} else {
result._flags &= ~(Flag::Chatlist | Flag::HasMyLinks);
}
return result;
}
@ -593,7 +592,7 @@ bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) {
const auto id = filter.id();
const auto exceptionsChanged = filter.always() != updated.always();
const auto rulesMask = ~(Flag::Chatlist | Flag::HasMyLinks);
const auto rulesMask = Flag() | Flag::RulesMask;
const auto rulesChanged = exceptionsChanged
|| ((filter.flags() & rulesMask) != (updated.flags() & rulesMask))
|| (filter.never() != updated.never());

View File

@ -36,9 +36,13 @@ public:
NoMuted = (1 << 5),
NoRead = (1 << 6),
NoArchived = (1 << 7),
RulesMask = ((1 << 8) - 1),
Chatlist = (1 << 8),
HasMyLinks = (1 << 9),
NewChats = (1 << 10), // Telegram Business exceptions.
ExistingChats = (1 << 11),
};
friend constexpr inline bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;

View File

@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/abstract_box.h"
#include "passport/passport_form_controller.h"
#include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name
#include "data/business/data_business_chatbots.h"
#include "data/stickers/data_stickers.h"
#include "data/notify/data_notify_settings.h"
#include "data/data_bot_app.h"
@ -268,7 +269,8 @@ Session::Session(not_null<Main::Session*> session)
, _notifySettings(std::make_unique<NotifySettings>(this))
, _customEmojiManager(std::make_unique<CustomEmojiManager>(this))
, _stories(std::make_unique<Stories>(this))
, _savedMessages(std::make_unique<SavedMessages>(this)) {
, _savedMessages(std::make_unique<SavedMessages>(this))
, _chatbots(std::make_unique<Chatbots>(this)) {
_cache->open(_session->local().cacheKey());
_bigFileCache->open(_session->local().cacheBigFileKey());

View File

@ -62,6 +62,7 @@ class NotifySettings;
class CustomEmojiManager;
class Stories;
class SavedMessages;
class Chatbots;
struct ReactionId;
struct RepliesReadTillUpdate {
@ -142,6 +143,9 @@ public:
[[nodiscard]] SavedMessages &savedMessages() const {
return *_savedMessages;
}
[[nodiscard]] Chatbots &chatbots() const {
return *_chatbots;
}
[[nodiscard]] MsgId nextNonHistoryEntryId() {
return ++_nonHistoryEntryId;
@ -1065,6 +1069,7 @@ private:
const std::unique_ptr<CustomEmojiManager> _customEmojiManager;
const std::unique_ptr<Stories> _stories;
const std::unique_ptr<SavedMessages> _savedMessages;
const std::unique_ptr<Chatbots> _chatbots;
MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange;

View File

@ -0,0 +1,145 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/business/settings_business_exceptions.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "boxes/filters/edit_filter_chats_preview.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
namespace Settings {
namespace {
using Flag = Data::ChatFilter::Flag;
using Flags = Data::ChatFilter::Flags;
[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) {
using Type = Data::BusinessChatType;
return ((types & Type::Contacts) ? Flag::Contacts : Flag())
| ((types & Type::NonContacts) ? Flag::NonContacts : Flag())
| ((types & Type::NewChats) ? Flag::NewChats : Flag())
| ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag());
}
[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) {
using Type = Data::BusinessChatType;
return ((flags & Flag::Contacts) ? Type::Contacts : Type())
| ((flags & Flag::NonContacts) ? Type::NonContacts : Type())
| ((flags & Flag::NewChats) ? Type::NewChats : Type())
| ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type());
}
} // namespace
void EditBusinessExceptions(
not_null<Window::SessionController*> window,
BusinessExceptionsDescriptor &&descriptor) {
const auto session = &window->session();
const auto options = Flag::ExistingChats
| Flag::NewChats
| Flag::Contacts
| Flag::NonContacts;
auto &&peers = descriptor.current.list | ranges::views::transform([=](
not_null<UserData*> user) {
return user->owner().history(user);
});
auto controller = std::make_unique<EditFilterChatsListController>(
session,
(descriptor.allow
? tr::lng_filters_include_title()
: tr::lng_filters_exclude_title()),
options,
TypesToFlags(descriptor.current.types) & options,
base::flat_set<not_null<History*>>(begin(peers), end(peers)),
[=](int count) {
return nullptr; AssertIsDebug();
});
const auto rawController = controller.get();
const auto save = descriptor.save;
auto initBox = [=](not_null<PeerListBox*> box) {
box->setCloseByOutsideClick(false);
box->addButton(tr::lng_settings_save(), crl::guard(box, [=] {
const auto peers = box->collectSelectedRows();
auto &&users = ranges::views::all(
peers
) | ranges::views::transform([=](not_null<PeerData*> peer) {
return not_null(peer->asUser());
}) | ranges::to_vector;
save(Data::BusinessExceptions{
.types = FlagsToTypes(rawController->chosenOptions()),
.list = std::move(users),
});
box->closeBox();
}));
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
};
window->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
}
not_null<FilterChatsPreview*> SetupBusinessExceptionsPreview(
not_null<Ui::VerticalLayout*> content,
not_null<rpl::variable<Data::BusinessExceptions>*> data) {
const auto rules = data->current();
const auto locked = std::make_shared<bool>();
auto &&peers = data->current().list | ranges::views::transform([=](
not_null<UserData*> user) {
return user->owner().history(user);
});
const auto preview = content->add(object_ptr<FilterChatsPreview>(
content,
TypesToFlags(data->current().types),
base::flat_set<not_null<History*>>(begin(peers), end(peers))));
preview->flagRemoved(
) | rpl::start_with_next([=](Flag flag) {
*locked = true;
*data = Data::BusinessExceptions{
data->current().types & ~FlagsToTypes(flag),
data->current().list
};
*locked = false;
}, preview->lifetime());
preview->peerRemoved(
) | rpl::start_with_next([=](not_null<History*> history) {
auto list = data->current().list;
list.erase(
ranges::remove(list, not_null(history->peer->asUser())),
end(list));
*locked = true;
*data = Data::BusinessExceptions{
data->current().types,
std::move(list)
};
*locked = false;
}, preview->lifetime());
data->changes(
) | rpl::filter([=] {
return !*locked;
}) | rpl::start_with_next([=](const Data::BusinessExceptions &rules) {
auto &&peers = rules.list | ranges::views::transform([=](
not_null<UserData*> user) {
return user->owner().history(user);
});
preview->updateData(
TypesToFlags(rules.types),
base::flat_set<not_null<History*>>(begin(peers), end(peers)));
}, preview->lifetime());
return preview;
}
} // namespace Settings

View File

@ -0,0 +1,37 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/business/data_business_common.h"
class FilterChatsPreview;
namespace Ui {
class VerticalLayout;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Settings {
struct BusinessExceptionsDescriptor {
Data::BusinessExceptions current;
Fn<void(const Data::BusinessExceptions&)> save;
bool allow = false;
};
void EditBusinessExceptions(
not_null<Window::SessionController*> window,
BusinessExceptionsDescriptor &&descriptor);
not_null<FilterChatsPreview*> SetupBusinessExceptionsPreview(
not_null<Ui::VerticalLayout*> content,
not_null<rpl::variable<Data::BusinessExceptions>*> data);
} // namespace Settings

View File

@ -7,7 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/business/settings_chatbots.h"
#include "core/application.h"
#include "data/business/data_business_chatbots.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/business/settings_business_exceptions.h"
#include "settings/settings_common_session.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
@ -15,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
@ -29,6 +36,7 @@ public:
Chatbots(
QWidget *parent,
not_null<Window::SessionController*> controller);
~Chatbots();
[[nodiscard]] rpl::producer<QString> title() override;
@ -42,24 +50,41 @@ public:
private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
void showFinished() override {
_showFinished.fire({});
}
const not_null<Window::SessionController*> _controller;
const not_null<Main::Session*> _session;
rpl::event_stream<> _showFinished;
Ui::RoundRect _bottomSkipRounding;
rpl::variable<bool> _onlySelected = false;
rpl::variable<bool> _repliesAllowed = true;
rpl::variable<Data::BusinessExceptions> _allowed;
rpl::variable<Data::BusinessExceptions> _disallowed;
};
Chatbots::Chatbots(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent)
, _controller(controller)
, _session(&controller->session())
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
setupContent(controller);
}
Chatbots::~Chatbots() {
if (!Core::Quitting()) {
save();
}
}
rpl::producer<QString> Chatbots::title() {
return tr::lng_chatbots_title();
}
@ -69,12 +94,12 @@ void Chatbots::setupContent(
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto current = controller->session().data().chatbots().current();
struct State {
rpl::variable<bool> onlySelected = false;
rpl::variable<bool> replyAllowed = true;
};
const auto state = content->lifetime().make_state<State>();
_onlySelected = current.onlySelected;
_repliesAllowed = current.repliesAllowed;
_allowed = current.allowed;
_disallowed = current.disallowed;
AddDividerTextWithLottie(content, {
.lottie = u"robot"_q,
@ -93,7 +118,11 @@ void Chatbots::setupContent(
object_ptr<Ui::InputField>(
content,
st::settingsChatbotsUsername,
tr::lng_chatbots_placeholder()),
tr::lng_chatbots_placeholder(),
(current.bot
? current.bot->session().createInternalLink(
current.bot->username())
: QString())),
st::settingsChatbotsUsernameMargins);
Ui::AddDividerText(
@ -104,7 +133,7 @@ void Chatbots::setupContent(
Ui::AddSubsectionTitle(content, tr::lng_chatbots_access_title());
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
state->onlySelected.current() ? kSelectedOnly : kAllExcept);
_onlySelected.current() ? kSelectedOnly : kAllExcept);
const auto everyone = content->add(
object_ptr<Ui::Radiobutton>(
content,
@ -139,8 +168,18 @@ void Chatbots::setupContent(
tr::lng_chatbots_exclude_button(),
st::settingsChatbotsAdd,
{ &st::settingsIconRemove, IconType::Round, &st::windowBgActive });
excludeAdd->setClickedCallback([=] {
EditBusinessExceptions(_controller, {
.current = _disallowed.current(),
.save = crl::guard(this, [=](Data::BusinessExceptions value) {
_disallowed = std::move(value);
}),
.allow = false,
});
});
SetupBusinessExceptionsPreview(excludeInner, &_disallowed);
excludeWrap->toggleOn(state->onlySelected.value() | rpl::map(!_1));
excludeWrap->toggleOn(_onlySelected.value() | rpl::map(!_1));
excludeWrap->finishAnimating();
const auto includeWrap = content->add(
@ -157,12 +196,22 @@ void Chatbots::setupContent(
tr::lng_chatbots_include_button(),
st::settingsChatbotsAdd,
{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });
includeAdd->setClickedCallback([=] {
EditBusinessExceptions(_controller, {
.current = _allowed.current(),
.save = crl::guard(this, [=](Data::BusinessExceptions value) {
_allowed = std::move(value);
}),
.allow = true,
});
});
SetupBusinessExceptionsPreview(includeInner, &_allowed);
includeWrap->toggleOn(state->onlySelected.value());
includeWrap->toggleOn(_onlySelected.value());
includeWrap->finishAnimating();
group->setChangedCallback([=](int value) {
state->onlySelected = (value == kSelectedOnly);
_onlySelected = (value == kSelectedOnly);
});
Ui::AddSkip(content, st::settingsChatbotsAccessSkip);
@ -177,9 +226,9 @@ void Chatbots::setupContent(
content,
tr::lng_chatbots_reply(),
st::settingsButtonNoIcon
))->toggleOn(state->replyAllowed.value())->toggledChanges(
))->toggleOn(_repliesAllowed.value())->toggledChanges(
) | rpl::start_with_next([=](bool value) {
state->replyAllowed = value;
_repliesAllowed = value;
}, content->lifetime());
Ui::AddSkip(content);
@ -192,6 +241,17 @@ void Chatbots::setupContent(
Ui::ResizeFitChild(this, content);
}
void Chatbots::save() {
const auto settings = Data::ChatbotsSettings{
.bot = nullptr,
.allowed = _allowed.current(),
.disallowed = _disallowed.current(),
.repliesAllowed = _repliesAllowed.current(),
.onlySelected = _onlySelected.current(),
};
_session->data().chatbots().save(settings);
}
} // namespace
Type ChatbotsId() {

View File

@ -303,6 +303,8 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg
windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }};
windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }};
windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }};
windowFilterTypeNewChats: icon {{ "folders/folders_unread", historyPeerUserpicFg }};
windowFilterTypeExistingChats: windowFilterTypeNoRead;
windowFilterChatsSectionSubtitleHeight: 28px;
windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {