Initial chat links edition implementation.

This commit is contained in:
John Preston 2024-03-26 14:00:34 +04:00
parent 6fe0c60204
commit 3d54f8ec49
23 changed files with 1250 additions and 53 deletions

View File

@ -110,6 +110,8 @@ PRIVATE
api/api_chat_filters.h
api/api_chat_invite.cpp
api/api_chat_invite.h
api/api_chat_links.cpp
api/api_chat_links.h
api/api_chat_participants.cpp
api/api_chat_participants.h
api/api_cloud_password.cpp
@ -1297,6 +1299,8 @@ PRIVATE
settings/business/settings_shortcut_messages.h
settings/business/settings_chat_intro.cpp
settings/business/settings_chat_intro.h
settings/business/settings_chat_links.cpp
settings/business/settings_chat_links.h
settings/business/settings_chatbots.cpp
settings/business/settings_chatbots.h
settings/business/settings_greeting.cpp

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -2211,8 +2211,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_business_about_away_messages" = "Define messages that are automatically sent when you are off.";
"lng_business_subtitle_chatbots" = "Chatbots";
"lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions.";
"lng_business_subtitle_chat_intro" = "Intro";
"lng_business_subtitle_chat_intro" = "Custom Intro";
"lng_business_about_chat_intro" = "Customize the message people see before they start a chat with you.";
"lng_business_subtitle_chat_links" = "Links to Chat";
"lng_business_about_chat_links" = "Create links that start a chat with you, suggesting the first message.";
"lng_location_title" = "Location";
"lng_location_about" = "Display the location of your business on your account.";
@ -2325,7 +2327,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_chatbot_menu_remove" = "Remove bot from this chat";
"lng_chatbot_menu_revoke" = "Revoke access to this chat";
"lng_chat_intro_title" = "Intro";
"lng_chat_intro_title" = "Custom Intro";
"lng_chat_intro_subtitle" = "Customize your intro";
"lng_chat_intro_default_title" = "No messages here yet...";
"lng_chat_intro_default_message" = "Send a message or click on the greeting below";
@ -2336,6 +2338,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_chat_intro_about" = "You can customize the message people see before they start a chat with you.";
"lng_chat_intro_reset" = "Reset to Default";
"lng_chat_links_title" = "Links to Chat";
"lng_chat_links_about" = "Give your customers short links that start a chat with you and suggest the first message from them to you.";
"lng_chat_links_create_link" = "Create a Link to Chat";
"lng_chat_links_footer" = "You can also use a simple link for a chat with you {links}";
"lng_chat_links_footer_both" = "{username} or {link}";
"lng_chat_links_no_clicks" = "no clicks";
"lng_chat_links_clicks#one" = "{count} click";
"lng_chat_links_clicks#other" = "{count} clicks";
"lng_chat_link_new_title" = "New Link";
"lng_chat_link_edit_title" = "Edit Link";
"lng_chat_link_description" = "Add a message that will be entered in the message field for anyone who starts a chat with you using this link.";
"lng_chat_link_placeholder" = "Add Preset Message";
"lng_chat_link_saved" = "Chat link saved.";
"lng_chat_link_copy" = "Copy";
"lng_chat_link_share" = "Share";
"lng_chat_link_rename" = "Rename";
"lng_chat_link_delete" = "Delete";
"lng_chat_link_name" = "Link Name (optional)";
"lng_chat_link_name_about" = "Add a name for this link that only you will see.";
"lng_chat_link_delete_sure" = "Are you sure you want to delete this chat link?";
"lng_chat_link_qr_title" = "Chat Link QR Code";
"lng_chat_link_qr_about" = "Everyone on Telegram can scan this code to contact you.";
"lng_chat_link_copied" = "Chat link copied to clipboard.";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_group_button" = "Boost Group";
"lng_boost_again_button" = "Boost Again";

View File

@ -21,5 +21,6 @@
<file alias="writing.tgs">../../animations/writing.tgs</file>
<file alias="hours.tgs">../../animations/hours.tgs</file>
<file alias="phone.tgs">../../animations/phone.tgs</file>
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
</qresource>
</RCC>

View File

@ -0,0 +1,171 @@
/*
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 "api/api_chat_links.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] ChatLink FromMTP(
not_null<Main::Session*> session,
const MTPBusinessChatLink &link) {
const auto &data = link.data();
return {
.link = qs(data.vlink()),
.title = qs(data.vtitle().value_or_empty()),
.message = {
qs(data.vmessage()),
EntitiesFromMTP(
session,
data.ventities().value_or_empty())
},
.clicks = data.vviews().v,
};
}
[[nodiscard]] MTPInputBusinessChatLink ToMTP(
not_null<Main::Session*> session,
const QString &title,
const TextWithEntities &message) {
auto entities = EntitiesToMTP(
session,
message.entities,
ConvertOption::SkipLocal);
using Flag = MTPDinputBusinessChatLink::Flag;
const auto flags = (title.isEmpty() ? Flag() : Flag::f_title)
| (entities.v.isEmpty() ? Flag() : Flag::f_entities);
return MTP_inputBusinessChatLink(
MTP_flags(flags),
MTP_string(message.text),
std::move(entities),
MTP_string(title));
}
} // namespace
ChatLinks::ChatLinks(not_null<ApiWrap*> api) : _api(api) {
}
void ChatLinks::create(
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done) {
const auto session = &_api->session();
_api->request(MTPaccount_CreateBusinessChatLink(
ToMTP(session, title, message)
)).done([=](const MTPBusinessChatLink &result) {
const auto link = FromMTP(session, result);
_list.push_back(link);
_updates.fire({ .was = QString(), .now = link });
if (done) done(link);
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (done) done(Link());
}).send();
}
void ChatLinks::edit(
const QString &link,
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done) {
const auto session = &_api->session();
_api->request(MTPaccount_EditBusinessChatLink(
MTP_string(link),
ToMTP(session, title, message)
)).done([=](const MTPBusinessChatLink &result) {
const auto parsed = FromMTP(session, result);
if (parsed.link != link) {
LOG(("API Error: EditBusinessChatLink changed the link."));
if (done) done(Link());
return;
}
const auto i = ranges::find(_list, link, &Link::link);
if (i != end(_list)) {
*i = parsed;
_updates.fire({ .was = link, .now = parsed });
if (done) done(parsed);
} else {
LOG(("API Error: EditBusinessChatLink link not found."));
if (done) done(Link());
}
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (done) done(Link());
}).send();
}
void ChatLinks::destroy(
const QString &link,
Fn<void()> done) {
_api->request(MTPaccount_DeleteBusinessChatLink(
MTP_string(link)
)).done([=] {
const auto i = ranges::find(_list, link, &Link::link);
if (i != end(_list)) {
_list.erase(i);
_updates.fire({ .was = link });
if (done) done();
} else {
LOG(("API Error: DeleteBusinessChatLink link not found."));
if (done) done();
}
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (done) done();
}).send();
}
void ChatLinks::preload() {
if (_loaded || _requestId) {
return;
}
_requestId = _api->request(MTPaccount_GetBusinessChatLinks(
)).done([=](const MTPaccount_BusinessChatLinks &result) {
const auto &data = result.data();
const auto session = &_api->session();
const auto owner = &session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
auto links = std::vector<Link>();
links.reserve(data.vlinks().v.size());
for (const auto &link : data.vlinks().v) {
links.push_back(FromMTP(session, link));
}
_list = std::move(links);
_loaded = true;
_loadedUpdates.fire({});
}).fail([=] {
_requestId = 0;
_loaded = true;
_loadedUpdates.fire({});
}).send();
}
const std::vector<ChatLink> &ChatLinks::list() const {
return _list;
}
bool ChatLinks::loaded() const {
return _loaded;
}
rpl::producer<> ChatLinks::loadedUpdates() const {
return _loadedUpdates.events();
}
rpl::producer<ChatLinks::Update> ChatLinks::updates() const {
return _updates.events();
}
} // namespace Api

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
class ApiWrap;
namespace Api {
struct ChatLink {
QString link;
QString title;
TextWithEntities message;
int clicks = 0;
};
struct ChatLinkUpdate {
QString was;
std::optional<ChatLink> now;
};
class ChatLinks final {
public:
explicit ChatLinks(not_null<ApiWrap*> api);
using Link = ChatLink;
using Update = ChatLinkUpdate;
void create(
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done = nullptr);
void edit(
const QString &link,
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done = nullptr);
void destroy(
const QString &link,
Fn<void()> done = nullptr);
void preload();
[[nodiscard]] const std::vector<ChatLink> &list() const;
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<> loadedUpdates() const;
[[nodiscard]] rpl::producer<Update> updates() const;
private:
const not_null<ApiWrap*> _api;
std::vector<Link> _list;
rpl::event_stream<> _loadedUpdates;
mtpRequestId _requestId = 0;
bool _loaded = false;
rpl::event_stream<Update> _updates;
};
} // namespace Api

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_authorizations.h"
#include "api/api_attached_stickers.h"
#include "api/api_blocked_peers.h"
#include "api/api_chat_links.h"
#include "api/api_chat_participants.h"
#include "api/api_cloud_password.h"
#include "api/api_hash.h"
@ -163,6 +164,7 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
, _globalPrivacy(std::make_unique<Api::GlobalPrivacy>(this))
, _userPrivacy(std::make_unique<Api::UserPrivacy>(this))
, _inviteLinks(std::make_unique<Api::InviteLinks>(this))
, _chatLinks(std::make_unique<Api::ChatLinks>(this))
, _views(std::make_unique<Api::ViewsManager>(this))
, _confirmPhone(std::make_unique<Api::ConfirmPhone>(this))
, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
@ -4424,6 +4426,10 @@ Api::InviteLinks &ApiWrap::inviteLinks() {
return *_inviteLinks;
}
Api::ChatLinks &ApiWrap::chatLinks() {
return *_chatLinks;
}
Api::ViewsManager &ApiWrap::views() {
return *_views;
}

View File

@ -69,6 +69,7 @@ class SensitiveContent;
class GlobalPrivacy;
class UserPrivacy;
class InviteLinks;
class ChatLinks;
class ViewsManager;
class ConfirmPhone;
class PeerPhoto;
@ -384,6 +385,7 @@ public:
[[nodiscard]] Api::GlobalPrivacy &globalPrivacy();
[[nodiscard]] Api::UserPrivacy &userPrivacy();
[[nodiscard]] Api::InviteLinks &inviteLinks();
[[nodiscard]] Api::ChatLinks &chatLinks();
[[nodiscard]] Api::ViewsManager &views();
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
[[nodiscard]] Api::PeerPhoto &peerPhoto();
@ -703,6 +705,7 @@ private:
const std::unique_ptr<Api::GlobalPrivacy> _globalPrivacy;
const std::unique_ptr<Api::UserPrivacy> _userPrivacy;
const std::unique_ptr<Api::InviteLinks> _inviteLinks;
const std::unique_ptr<Api::ChatLinks> _chatLinks;
const std::unique_ptr<Api::ViewsManager> _views;
const std::unique_ptr<Api::ConfirmPhone> _confirmPhone;
const std::unique_ptr<Api::PeerPhoto> _peerPhoto;

View File

@ -580,8 +580,10 @@ void LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {
ShareInviteLinkBox(&_window->session(), link));
});
const auto getLinkQr = crl::guard(weak, [=] {
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_filters_link_qr_about()));
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
link,
tr::lng_group_invite_qr_title(),
tr::lng_filters_link_qr_about()));
});
const auto editLink = crl::guard(weak, [=] {
delegate()->peerListUiShow()->showBox(
@ -886,8 +888,10 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
ShareInviteLinkBox(&_window->session(), link));
};
const auto getLinkQr = [=] {
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_filters_link_qr_about()));
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
link,
tr::lng_group_invite_qr_title(),
tr::lng_filters_link_qr_about()));
};
const auto editLink = [=] {
delegate()->peerListUiShow()->showBox(

View File

@ -272,9 +272,10 @@ QImage QrForShare(const QString &text) {
void QrBox(
not_null<Ui::GenericBox*> box,
const QString &link,
rpl::producer<QString> title,
rpl::producer<QString> about,
Fn<void(QImage, std::shared_ptr<Ui::Show>)> share) {
box->setTitle(tr::lng_group_invite_qr_title());
box->setTitle(std::move(title));
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
@ -350,8 +351,10 @@ void Controller::addHeaderBlock(not_null<Ui::VerticalLayout*> container) {
delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(peer, link));
});
const auto getLinkQr = crl::guard(weak, [=] {
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_group_invite_qr_about()));
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
link,
tr::lng_group_invite_qr_title(),
tr::lng_group_invite_qr_about()));
});
const auto revokeLink = crl::guard(weak, [=] {
delegate()->peerListUiShow()->showBox(
@ -976,6 +979,7 @@ void AddPermanentLinkBlock(
if (const auto current = value->current(); !current.link.isEmpty()) {
show->showBox(InviteLinkQrBox(
current.link,
tr::lng_group_invite_qr_title(),
tr::lng_group_invite_qr_about()));
}
});
@ -1130,13 +1134,15 @@ void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link) {
object_ptr<Ui::BoxContent> ShareInviteLinkBox(
not_null<PeerData*> peer,
const QString &link) {
return ShareInviteLinkBox(&peer->session(), link);
const QString &link,
const QString &copied) {
return ShareInviteLinkBox(&peer->session(), link, copied);
}
object_ptr<Ui::BoxContent> ShareInviteLinkBox(
not_null<Main::Session*> session,
const QString &link) {
const QString &link,
const QString &copied) {
const auto sending = std::make_shared<bool>();
const auto box = std::make_shared<QPointer<ShareBox>>();
@ -1148,7 +1154,9 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
auto copyCallback = [=] {
QGuiApplication::clipboard()->setText(link);
showToast(tr::lng_group_invite_copied(tr::now));
showToast(copied.isEmpty()
? tr::lng_group_invite_copied(tr::now)
: copied);
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
@ -1228,8 +1236,9 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
object_ptr<Ui::BoxContent> InviteLinkQrBox(
const QString &link,
rpl::producer<QString> title,
rpl::producer<QString> about) {
return Box(QrBox, link, std::move(about), [=](
return Box(QrBox, link, std::move(title), std::move(about), [=](
const QImage &image,
std::shared_ptr<Ui::Show> show) {
auto mime = std::make_unique<QMimeData>();

View File

@ -41,12 +41,15 @@ void AddPermanentLinkBlock(
void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link);
[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(
not_null<PeerData*> peer,
const QString &link);
const QString &link,
const QString &copied = {});
[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(
not_null<Main::Session*> session,
const QString &link);
const QString &link,
const QString &copied = {});
[[nodiscard]] object_ptr<Ui::BoxContent> InviteLinkQrBox(
const QString &link,
rpl::producer<QString> title,
rpl::producer<QString> about);
[[nodiscard]] object_ptr<Ui::BoxContent> RevokeLinkBox(
not_null<PeerData*> peer,

View File

@ -214,38 +214,11 @@ object_ptr<Ui::BoxContent> DeleteAllRevokedBox(
});
}
not_null<Ui::SettingsButton*> AddCreateLinkButton(
[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateLinkButton(
not_null<Ui::VerticalLayout*> container) {
const auto result = container->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_group_invite_add(),
st::inviteLinkCreate),
return container->add(
MakeCreateLinkButton(container, tr::lng_group_invite_add()),
style::margins(0, st::inviteLinkCreateSkip, 0, 0));
const auto icon = Ui::CreateChild<Ui::RpWidget>(result);
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto size = st::inviteLinkCreateIconSize;
icon->resize(size, size);
result->heightValue(
) | rpl::start_with_next([=](int height) {
const auto &st = st::inviteLinkList.item;
icon->move(
st.photoPosition.x() + (st.photoSize - size) / 2,
(height - size) / 2);
}, icon->lifetime());
icon->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(icon);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgActive);
const auto rect = icon->rect();
{
auto hq = PainterHighQualityEnabler(p);
p.drawEllipse(rect);
}
st::inviteLinkCreateIcon.paintInCenter(p, rect);
}, icon->lifetime());
return result;
}
Row::Row(
@ -584,8 +557,10 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
ShareInviteLinkBox(_peer, link));
}, &st::menuIconShare);
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
delegate()->peerListUiShow()->showBox(
InviteLinkQrBox(link, tr::lng_group_invite_qr_about()));
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
link,
tr::lng_group_invite_qr_title(),
tr::lng_group_invite_qr_about()));
}, &st::menuIconQrCode);
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
delegate()->peerListUiShow()->showBox(EditLinkBox(_peer, data));
@ -1014,3 +989,42 @@ void ManageInviteLinksBox(
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
}
object_ptr<Ui::SettingsButton> MakeCreateLinkButton(
not_null<QWidget*> parent,
rpl::producer<QString> text) {
auto result = object_ptr<Ui::SettingsButton>(
parent,
std::move(text),
st::inviteLinkCreate);
const auto raw = result.data();
const auto icon = Ui::CreateChild<Ui::RpWidget>(raw);
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto size = st::inviteLinkCreateIconSize;
icon->resize(size, size);
raw->heightValue(
) | rpl::start_with_next([=](int height) {
const auto &st = st::inviteLinkList.item;
icon->move(
st.photoPosition.x() + (st.photoSize - size) / 2,
(height - size) / 2);
}, icon->lifetime());
icon->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(icon);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgActive);
const auto rect = icon->rect();
{
auto hq = PainterHighQualityEnabler(p);
p.drawEllipse(rect);
}
st::inviteLinkCreateIcon.paintInCenter(p, rect);
}, icon->lifetime());
return result;
}

View File

@ -11,9 +11,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class PeerData;
namespace Ui {
class SettingsButton;
} // namespace Ui
void ManageInviteLinksBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
not_null<UserData*> admin,
int count,
int revokedCount);
[[nodiscard]] object_ptr<Ui::SettingsButton> MakeCreateLinkButton(
not_null<QWidget*> parent,
rpl::producer<QString> text);

View File

@ -146,6 +146,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_business_subtitle_chatbots();
case PremiumFeature::ChatIntro:
return tr::lng_business_subtitle_chat_intro();
case PremiumFeature::ChatLinks:
return tr::lng_business_subtitle_chat_links();
}
Unexpected("PremiumFeature in SectionTitle.");
}
@ -205,6 +207,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_business_about_chatbots();
case PremiumFeature::ChatIntro:
return tr::lng_business_about_chat_intro();
case PremiumFeature::ChatLinks:
return tr::lng_business_about_chat_links();
}
Unexpected("PremiumFeature in SectionTitle.");
}
@ -533,6 +537,7 @@ struct VideoPreviewDocument {
case PremiumFeature::AwayMessage: return "away_message";
case PremiumFeature::BusinessBots: return "business_bots";
case PremiumFeature::ChatIntro: return "business_intro";
case PremiumFeature::ChatLinks: return "business_links";
}
return "";
}();

View File

@ -75,6 +75,7 @@ enum class PremiumFeature {
AwayMessage,
BusinessBots,
ChatIntro,
ChatLinks,
kCount,
};

View File

@ -0,0 +1,813 @@
/*
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_chat_links.h"
#include "api/api_chat_links.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "boxes/peers/edit_peer_invite_link.h"
#include "boxes/peers/edit_peer_invite_links.h"
#include "boxes/premium_preview_box.h"
#include "boxes/peer_list_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "core/core_settings.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/emoji_button.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/popup_menu.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_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include <QtGui/QGuiApplication>
namespace Settings {
namespace {
constexpr auto kChangesDebounceTimeout = crl::time(1000);
using ChatLinkData = Api::ChatLink;
class ChatLinks final : public BusinessSection<ChatLinks> {
public:
ChatLinks(
QWidget *parent,
not_null<Window::SessionController*> controller);
~ChatLinks();
[[nodiscard]] rpl::producer<QString> title() override;
const Ui::RoundRect *bottomSkipRounding() const override {
return &_bottomSkipRounding;
}
private:
void setupContent(not_null<Window::SessionController*> controller);
Ui::RoundRect _bottomSkipRounding;
};
struct ChatLinkAction {
enum class Type {
Copy,
Share,
Rename,
Delete,
};
QString link;
Type type = Type::Copy;
};
class Row;
class RowDelegate {
public:
virtual not_null<Main::Session*> rowSession() = 0;
virtual void rowUpdateRow(not_null<Row*> row) = 0;
virtual void rowPaintIcon(
QPainter &p,
int x,
int y,
int size) = 0;
};
class Row final : public PeerListRow {
public:
Row(not_null<RowDelegate*> delegate, const ChatLinkData &data);
void update(const ChatLinkData &data);
[[nodiscard]] ChatLinkData data() const;
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
bool rightActionDisabled() const override {
return true;
}
void paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) override;
private:
void updateStatus(const ChatLinkData &data);
const not_null<RowDelegate*> _delegate;
ChatLinkData _data;
Ui::Text::String _status;
Ui::Text::String _clicks;
};
[[nodiscard]] uint64 ComputeRowId(const ChatLinkData &data) {
return UniqueRowIdFromString(data.link);
}
[[nodiscard]] QString ComputeClicks(const ChatLinkData &link) {
return link.clicks
? tr::lng_chat_links_clicks(tr::now, lt_count, link.clicks)
: tr::lng_chat_links_no_clicks(tr::now);
}
Row::Row(not_null<RowDelegate*> delegate, const ChatLinkData &data)
: PeerListRow(ComputeRowId(data))
, _delegate(delegate)
, _data(data) {
setCustomStatus(QString());
updateStatus(data);
}
void Row::updateStatus(const ChatLinkData &data) {
const auto context = Core::MarkedTextContext{
.session = _delegate->rowSession(),
.customEmojiRepaint = [=] { _delegate->rowUpdateRow(this); },
};
_status.setMarkedText(
st::messageTextStyle,
data.message,
kMarkupTextOptions,
context);
_clicks.setText(st::messageTextStyle, ComputeClicks(data));
}
void Row::update(const ChatLinkData &data) {
_data = data;
updateStatus(data);
refreshName(st::inviteLinkList.item);
_delegate->rowUpdateRow(this);
}
ChatLinkData Row::data() const {
return _data;
}
QString Row::generateName() {
if (!_data.title.isEmpty()) {
return _data.title;
}
auto result = _data.link;
return result.replace(
u"https://"_q,
QString()
);
}
QString Row::generateShortName() {
return generateName();
}
PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
return [=](
QPainter &p,
int x,
int y,
int outerWidth,
int size) {
_delegate->rowPaintIcon(p, x, y, size);
};
}
QSize Row::rightActionSize() const {
return QSize(
_clicks.maxWidth(),
st::inviteLinkThreeDotsIcon.height());
}
QMargins Row::rightActionMargins() const {
return QMargins(
0,
(st::inviteLinkList.item.height - rightActionSize().height()) / 2,
st::inviteLinkThreeDotsSkip,
0);
}
void Row::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);
_clicks.draw(p, x, y, outerWidth);
}
void Row::paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) {
p.setPen(selected ? st.statusFgOver : st.statusFg);
_status.draw(p, {
.position = { x, y },
.outerWidth = outerWidth,
.availableWidth = availableWidth,
.palette = &st::defaultTextPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = crl::now(),
.elisionLines = 1,
});
}
class LinksController final
: public PeerListController
, public RowDelegate
, public base::has_weak_ptr {
public:
explicit LinksController(not_null<Window::SessionController*> window);
[[nodiscard]] rpl::producer<int> fullCountValue() const {
return _count.value();
}
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
not_null<Main::Session*> rowSession() override;
void rowUpdateRow(not_null<Row*> row) override;
void rowPaintIcon(
QPainter &p,
int x,
int y,
int size) override;
private:
void appendRow(const ChatLinkData &data);
void prependRow(const ChatLinkData &data);
void updateRow(const ChatLinkData &data);
bool removeRow(const QString &link);
void showRowMenu(
not_null<PeerListRow*> row,
bool highlightRow);
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row);
const not_null<Window::SessionController*> _window;
const not_null<Main::Session*> _session;
rpl::variable<int> _count;
base::unique_qptr<Ui::PopupMenu> _menu;
QImage _icon;
rpl::lifetime _lifetime;
};
struct LinksList {
not_null<Ui::RpWidget*> widget;
not_null<LinksController*> controller;
};
LinksList AddLinksList(
not_null<Window::SessionController*> window,
not_null<Ui::VerticalLayout*> container) {
auto &lifetime = container->lifetime();
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
window->uiShow());
const auto controller = lifetime.make_state<LinksController>(window);
controller->setStyleOverrides(&st::inviteLinkList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
return { content, controller };
}
void EditChatLinkBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
ChatLinkData data,
Fn<void(ChatLinkData, Fn<void()> close)> submit) {
box->setTitle(data.link.isEmpty()
? tr::lng_chat_link_new_title()
: tr::lng_chat_link_edit_title());
box->setWidth(st::boxWideWidth);
Ui::AddDividerText(
box->verticalLayout(),
tr::lng_chat_link_description());
const auto peer = controller->session().user();
const auto outer = box->getDelegate()->outerContainer();
const auto field = box->addRow(
object_ptr<Ui::InputField>(
box.get(),
st::settingsChatLinkField,
Ui::InputField::Mode::MultiLine,
tr::lng_chat_link_placeholder()));
box->setFocusCallback([=] {
field->setFocusFast();
});
Ui::AddDivider(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
const auto title = box->addRow(object_ptr<Ui::InputField>(
box.get(),
st::defaultInputField,
tr::lng_chat_link_name(),
data.title));
const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
field->parentWidget(),
st::defaultComposeFiles.emoji);
using Selector = ChatHelpers::TabbedSelector;
auto &lifetime = box->lifetime();
const auto emojiPanel = lifetime.make_state<ChatHelpers::TabbedPanel>(
outer,
controller,
object_ptr<Selector>(
nullptr,
controller->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::EmojiOnly));
emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
emojiPanel->hide();
emojiPanel->selector()->setCurrentPeer(peer);
emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
}, field->lifetime());
emojiPanel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
Data::InsertCustomEmoji(field, data.document);
}, field->lifetime());
emojiToggle->installEventFilter(emojiPanel);
emojiToggle->addClickHandler([=] {
emojiPanel->toggleAnimated();
});
const auto allow = [](not_null<DocumentData*>) { return true; };
InitMessageFieldHandlers(
controller,
field,
Window::GifPauseReason::Layer,
allow);
Ui::Emoji::SuggestionsController::Init(
outer,
field,
&controller->session(),
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
field->setMaxHeight(st::defaultComposeFiles.caption.heightMax);
const auto save = [=] {
auto copy = data;
copy.title = title->getLastText().trimmed();
auto textWithTags = field->getTextWithAppliedMarkdown();
copy.message = TextWithEntities{
textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
};
submit(copy, crl::guard(box, [=] {
box->closeBox();
}));
};
const auto updateEmojiPanelGeometry = [=] {
const auto parent = emojiPanel->parentWidget();
const auto global = emojiToggle->mapToGlobal({ 0, 0 });
const auto local = parent->mapFromGlobal(global);
emojiPanel->moveBottomRight(
local.y(),
local.x() + emojiToggle->width() * 3);
};
const auto filterCallback = [=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Move || type == QEvent::Resize) {
// updateEmojiPanelGeometry uses not only container geometry, but
// also container children geometries that will be updated later.
crl::on_main(emojiPanel, updateEmojiPanelGeometry);
}
return base::EventFilterResult::Continue;
};
base::install_event_filter(emojiPanel, outer, filterCallback);
field->submits(
) | rpl::start_with_next([=] {
title->setFocus();
}, field->lifetime());
field->cancelled(
) | rpl::start_with_next([=] {
box->closeBox();
}, field->lifetime());
title->submits(
) | rpl::start_with_next(save, title->lifetime());
rpl::combine(
box->sizeValue(),
field->geometryValue()
) | rpl::start_with_next([=](QSize outer, QRect inner) {
emojiToggle->moveToLeft(
inner.x() + inner.width() - emojiToggle->width(),
inner.y() + st::settingsChatLinkEmojiTop);
emojiToggle->update();
crl::on_main(emojiPanel, updateEmojiPanelGeometry);
}, emojiToggle->lifetime());
const auto initial = TextWithTags{
data.message.text,
TextUtilities::ConvertEntitiesToTextTags(data.message.entities)
};
field->setTextWithTags(initial, Ui::InputField::HistoryAction::Clear);
auto cursor = field->textCursor();
cursor.movePosition(QTextCursor::End);
field->setTextCursor(cursor);
const auto checkChangedTimer = lifetime.make_state<base::Timer>([=] {
if (field->getTextWithAppliedMarkdown() == initial) {
box->setCloseByOutsideClick(true);
}
});
field->changes(
) | rpl::start_with_next([=] {
checkChangedTimer->callOnce(kChangesDebounceTimeout);
box->setCloseByOutsideClick(false);
}, field->lifetime());
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void EditChatLink(
not_null<Window::SessionController*> window,
not_null<Main::Session*> session,
ChatLinkData data) {
const auto submitting = std::make_shared<bool>();
const auto submit = [=](ChatLinkData data, Fn<void()> close) {
if (std::exchange(*submitting, true)) {
return;
}
const auto done = crl::guard(window, [=](const auto&) {
window->showToast(tr::lng_chat_link_saved(tr::now));
close();
});
session->api().chatLinks().edit(
data.link,
data.title,
data.message,
done);
};
window->show(Box(
EditChatLinkBox,
window,
data,
crl::guard(window, submit)));
}
LinksController::LinksController(
not_null<Window::SessionController*> window)
: _window(window)
, _session(&window->session()) {
style::PaletteChanged(
) | rpl::start_with_next([=] {
_icon = QImage();
}, _lifetime);
_session->api().chatLinks().updates(
) | rpl::start_with_next([=](const Api::ChatLinkUpdate &update) {
if (!update.now) {
if (removeRow(update.was)) {
delegate()->peerListRefreshRows();
}
} else if (update.was.isEmpty()) {
prependRow(*update.now);
delegate()->peerListRefreshRows();
} else {
updateRow(*update.now);
}
}, _lifetime);
}
void LinksController::prepare() {
auto &&list = _session->api().chatLinks().list()
| ranges::views::reverse;
for (const auto &link : list) {
appendRow(link);
}
delegate()->peerListRefreshRows();
}
void LinksController::rowClicked(not_null<PeerListRow*> row) {
showRowMenu(row, true);
}
void LinksController::showRowMenu(
not_null<PeerListRow*> row,
bool highlightRow) {
delegate()->peerListShowRowMenu(row, highlightRow);
}
void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true);
}
base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto result = createRowContextMenu(parent, row);
if (result) {
// First clear _menu value, so that we don't check row positions yet.
base::take(_menu);
// Here unique_qptr is used like a shared pointer, where
// not the last destroyed pointer destroys the object, but the first.
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
}
return result;
}
base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
const auto real = static_cast<Row*>(row.get());
const auto data = real->data();
const auto link = data.link;
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
QGuiApplication::clipboard()->setText(link);
delegate()->peerListUiShow()->showToast(
tr::lng_chat_link_copied(tr::now));
}, &st::menuIconCopy);
result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(
_session,
link,
tr::lng_chat_link_copied(tr::now)));
}, &st::menuIconShare);
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
link,
tr::lng_chat_link_qr_title(),
tr::lng_chat_link_qr_about()));
}, &st::menuIconQrCode);
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
EditChatLink(_window, _session, data);
}, &st::menuIconEdit);
result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
const auto sure = [=](Fn<void()> &&close) {
_window->session().api().chatLinks().destroy(link, close);
};
_window->show(Ui::MakeConfirmBox({
.text = tr::lng_chat_link_delete_sure(tr::now),
.confirmed = sure,
.confirmText = tr::lng_box_delete(tr::now),
}));
}, &st::menuIconDelete);
return result;
}
Main::Session &LinksController::session() const {
return *_session;
}
void LinksController::appendRow(const ChatLinkData &data) {
delegate()->peerListAppendRow(std::make_unique<Row>(this, data));
_count = _count.current() + 1;
}
void LinksController::prependRow(const ChatLinkData &data) {
delegate()->peerListPrependRow(std::make_unique<Row>(this, data));
_count = _count.current() + 1;
}
void LinksController::updateRow(const ChatLinkData &data) {
if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {
const auto real = static_cast<Row*>(row);
real->update(data);
delegate()->peerListUpdateRow(row);
}
}
bool LinksController::removeRow(const QString &link) {
const auto id = UniqueRowIdFromString(link);
if (const auto row = delegate()->peerListFindRow(id)) {
delegate()->peerListRemoveRow(row);
_count = std::max(_count.current() - 1, 0);
return true;
}
return false;
}
not_null<Main::Session*> LinksController::rowSession() {
return _session;
}
void LinksController::rowUpdateRow(not_null<Row*> row) {
delegate()->peerListUpdateRow(row);
}
void LinksController::rowPaintIcon(
QPainter &p,
int x,
int y,
int size) {
const auto skip = st::inviteLinkIconSkip;
const auto inner = size - 2 * skip;
const auto bg = &st::msgFile1Bg;
const auto stroke = st::inviteLinkIconStroke;
if (_icon.isNull()) {
_icon = QImage(
QSize(inner, inner) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_icon.fill(Qt::transparent);
_icon.setDevicePixelRatio(style::DevicePixelRatio());
auto p = QPainter(&_icon);
p.setPen(Qt::NoPen);
p.setBrush(*bg);
{
auto hq = PainterHighQualityEnabler(p);
auto rect = QRect(0, 0, inner, inner);
p.drawEllipse(rect);
}
st::inviteLinkIcon.paintInCenter(p, { 0, 0, inner, inner });
}
p.drawImage(x + skip, y + skip, _icon);
}
ChatLinks::ChatLinks(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller)
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
setupContent(controller);
}
ChatLinks::~ChatLinks() = default;
rpl::producer<QString> ChatLinks::title() {
return tr::lng_chat_links_title();
}
void ChatLinks::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddDividerTextWithLottie(content, {
.lottie = u"chat_link"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes() | rpl::take(1),
.about = tr::lng_chat_links_about(Ui::Text::WithEntities),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
Ui::AddSkip(content);
const auto limit = controller->session().account().appConfig().get<int>(
u"business_chat_links_limit"_q,
100);
const auto add = content->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
content,
MakeCreateLinkButton(
content,
tr::lng_chat_links_create_link()))
)->setDuration(0);
const auto list = AddLinksList(controller, content);
add->toggleOn(list.controller->fullCountValue() | rpl::map(_1 < limit));
add->finishAnimating();
add->entity()->setClickedCallback([=] {
if (!controller->session().premium()) {
ShowPremiumPreviewToBuy(
controller,
PremiumFeature::ChatLinks);
return;
}
const auto submitting = std::make_shared<bool>();
const auto submit = [=](ChatLinkData data, Fn<void()> close) {
if (std::exchange(*submitting, true)) {
return;
}
const auto done = [=](const auto&) {
controller->showToast(tr::lng_chat_link_saved(tr::now));
close();
};
controller->session().api().chatLinks().create(
data.title,
data.message,
done);
};
controller->show(Box(
EditChatLinkBox,
controller,
ChatLinkData(),
crl::guard(this, submit)));
});
Ui::AddSkip(content);
const auto self = controller->session().user();
const auto username = self->username();
const auto make = [&](std::vector<QString> links) {
Expects(!links.empty());
for (auto &link : links) {
link = controller->session().createInternalLink(link);
}
return (links.size() > 1)
? tr::lng_chat_links_footer_both(
tr::now,
lt_username,
Ui::Text::Link(links[0], "https://" + links[0]),
lt_link,
Ui::Text::Link(links[1], "https://" + links[1]),
Ui::Text::WithEntities)
: Ui::Text::Link(links[0], "https://" + links[0]);
};
auto links = !username.isEmpty()
? make({ username, '+' + self->phone() })
: make({ '+' + self->phone() });
Ui::AddDividerText(
content,
tr::lng_chat_links_footer(
lt_links,
rpl::single(std::move(links)),
Ui::Text::WithEntities),
st::settingsChatbotsBottomTextMargin,
RectPart::Top);
Ui::ResizeFitChild(this, content);
}
} // namespace
Type ChatLinksId() {
return ChatLinks::Id();
}
} // namespace Settings

View File

@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type ChatLinksId();
} // namespace Settings

View File

@ -111,8 +111,10 @@ void QuickReplies::setupContent(
showOther(ShortcutMessagesId(id));
close();
};
controller->show(
Box(EditShortcutNameBox, QString(), crl::guard(this, submit)));
controller->show(Box(
EditShortcutNameBox,
QString(),
crl::guard(this, submit)));
});
if (count > 0) {
AddSkip(addWrap);

View File

@ -111,6 +111,7 @@ settingsBusinessIconGreeting: icon {{ "settings/premium/status", settingsIconFg
settingsBusinessIconAway: icon {{ "settings/premium/business/business_away", settingsIconFg }};
settingsBusinessIconChatbots: icon {{ "settings/premium/business/business_chatbots", settingsIconFg }};
settingsBusinessIconChatIntro: icon {{ "settings/premium/intro", settingsIconFg }};
settingsBusinessIconChatLinks: icon {{ "settings/premium/links", settingsIconFg }};
settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) {
style: TextStyle(semiboldTextStyle) {
@ -648,3 +649,23 @@ settingsChatIntroField: InputField(defaultMultiSelectSearchField) {
textMargins: margins(2px, 0px, 32px, 0px);
}
settingsChatIntroFieldMargins: margins(20px, 15px, 20px, 8px);
settingsChatLinkEmojiTop: 2px;
settingsChatLinkField: InputField(defaultInputField) {
textBg: transparent;
textMargins: margins(2px, 8px, 2px, 8px);
placeholderFg: placeholderFg;
placeholderFgActive: placeholderFgActive;
placeholderFgError: placeholderFgActive;
placeholderMargins: margins(0px, 0px, 0px, 0px);
placeholderScale: 0.;
placeholderFont: normalFont;
border: 0px;
borderActive: 0px;
heightMin: 32px;
font: normalFont;
}

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/settings_business.h"
#include "api/api_chat_links.h"
#include "boxes/premium_preview_box.h"
#include "core/click_handler_types.h"
#include "data/business/data_business_info.h"
@ -24,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "settings/business/settings_away_message.h"
#include "settings/business/settings_chat_intro.h"
#include "settings/business/settings_chat_links.h"
#include "settings/business/settings_chatbots.h"
#include "settings/business/settings_greeting.h"
#include "settings/business/settings_location.h"
@ -41,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/new_badges.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "apiwrap.h"
@ -58,6 +61,7 @@ struct Entry {
rpl::producer<QString> title;
rpl::producer<QString> description;
PremiumFeature feature = PremiumFeature::BusinessLocation;
bool newBadge = false;
};
using Order = std::vector<QString>;
@ -70,7 +74,8 @@ using Order = std::vector<QString>;
u"business_hours"_q,
u"business_location"_q,
u"business_bots"_q,
u"intro"_q,
u"business_intro"_q,
u"business_links"_q,
};
}
@ -131,12 +136,23 @@ using Order = std::vector<QString>;
},
},
{
u"intro"_q,
u"business_intro"_q,
Entry{
&st::settingsBusinessIconChatIntro,
tr::lng_business_subtitle_chat_intro(),
tr::lng_business_about_chat_intro(),
PremiumFeature::ChatIntro,
true
},
},
{
u"business_links"_q,
Entry{
&st::settingsBusinessIconChatLinks,
tr::lng_business_subtitle_chat_links(),
tr::lng_business_about_chat_links(),
PremiumFeature::ChatLinks,
true
},
},
};
@ -177,6 +193,9 @@ void AddBusinessSummary(
descriptionPadding);
description->setAttribute(Qt::WA_TransparentForMouseEvents);
if (entry.newBadge) {
Ui::NewBadge::AddAfterLabel(content, label);
}
const auto dummy = Ui::CreateChild<Ui::AbstractButton>(content.get());
dummy->setAttribute(Qt::WA_TransparentForMouseEvents);
@ -374,6 +393,7 @@ void Business::setupContent() {
owner->chatbots().preload();
owner->businessInfo().preload();
owner->shortcutMessages().preloadShortcuts();
owner->session().api().chatLinks().preload();
Ui::AddSkip(content, st::settingsFromFileTop);
@ -387,6 +407,7 @@ void Business::setupContent() {
case PremiumFeature::QuickReplies: return QuickRepliesId();
case PremiumFeature::BusinessBots: return ChatbotsId();
case PremiumFeature::ChatIntro: return ChatIntroId();
case PremiumFeature::ChatLinks: return ChatLinksId();
}
Unexpected("Feature in showFeature.");
}());
@ -410,6 +431,8 @@ void Business::setupContent() {
return owner->chatbots().loaded();
case PremiumFeature::ChatIntro:
return owner->session().user()->isFullLoaded();
case PremiumFeature::ChatLinks:
return owner->session().api().chatLinks().loaded();
}
Unexpected("Feature in isReady.");
};
@ -429,7 +452,8 @@ void Business::setupContent() {
owner->chatbots().changes() | rpl::to_empty,
owner->session().changes().peerUpdates(
owner->session().user(),
Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty
Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty,
owner->session().api().chatLinks().loadedUpdates()
) | rpl::start_with_next(check, content->lifetime());
AddBusinessSummary(content, _controller, [=](PremiumFeature feature) {
@ -686,6 +710,8 @@ std::vector<PremiumFeature> BusinessFeaturesOrder(
return PremiumFeature::BusinessBots;
} else if (s == u"business_intro"_q) {
return PremiumFeature::ChatIntro;
} else if (s == "business_links"_q) {
return PremiumFeature::ChatLinks;
}
return PremiumFeature::kCount;
}) | ranges::views::filter([](PremiumFeature feature) {