diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 1fecca354..9f0a2493b 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -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
diff --git a/Telegram/Resources/animations/chat_link.tgs b/Telegram/Resources/animations/chat_link.tgs
new file mode 100644
index 000000000..21622df37
Binary files /dev/null and b/Telegram/Resources/animations/chat_link.tgs differ
diff --git a/Telegram/Resources/icons/settings/premium/links.png b/Telegram/Resources/icons/settings/premium/links.png
new file mode 100644
index 000000000..3e7913e15
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/links.png differ
diff --git a/Telegram/Resources/icons/settings/premium/links@2x.png b/Telegram/Resources/icons/settings/premium/links@2x.png
new file mode 100644
index 000000000..6ef36c850
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/links@2x.png differ
diff --git a/Telegram/Resources/icons/settings/premium/links@3x.png b/Telegram/Resources/icons/settings/premium/links@3x.png
new file mode 100644
index 000000000..c9b5c3ea1
Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/links@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 3d1ac3b3c..5cb49b2ab 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -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";
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index 12666b6fd..b63b6d15f 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -21,5 +21,6 @@
../../animations/writing.tgs
../../animations/hours.tgs
../../animations/phone.tgs
+ ../../animations/chat_link.tgs
diff --git a/Telegram/SourceFiles/api/api_chat_links.cpp b/Telegram/SourceFiles/api/api_chat_links.cpp
new file mode 100644
index 000000000..f8a3f1979
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_chat_links.cpp
@@ -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 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 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 api) : _api(api) {
+}
+
+
+void ChatLinks::create(
+ const QString &title,
+ const TextWithEntities &message,
+ Fn 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 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 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();
+ 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 &ChatLinks::list() const {
+ return _list;
+}
+
+bool ChatLinks::loaded() const {
+ return _loaded;
+}
+
+rpl::producer<> ChatLinks::loadedUpdates() const {
+ return _loadedUpdates.events();
+}
+
+rpl::producer ChatLinks::updates() const {
+ return _updates.events();
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_chat_links.h b/Telegram/SourceFiles/api/api_chat_links.h
new file mode 100644
index 000000000..34226eab9
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_chat_links.h
@@ -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 now;
+};
+
+class ChatLinks final {
+public:
+ explicit ChatLinks(not_null api);
+
+ using Link = ChatLink;
+ using Update = ChatLinkUpdate;
+
+ void create(
+ const QString &title,
+ const TextWithEntities &message,
+ Fn done = nullptr);
+ void edit(
+ const QString &link,
+ const QString &title,
+ const TextWithEntities &message,
+ Fn done = nullptr);
+ void destroy(
+ const QString &link,
+ Fn done = nullptr);
+
+ void preload();
+ [[nodiscard]] const std::vector &list() const;
+ [[nodiscard]] bool loaded() const;
+ [[nodiscard]] rpl::producer<> loadedUpdates() const;
+ [[nodiscard]] rpl::producer updates() const;
+
+private:
+ const not_null _api;
+
+ std::vector _list;
+ rpl::event_stream<> _loadedUpdates;
+ mtpRequestId _requestId = 0;
+ bool _loaded = false;
+
+ rpl::event_stream _updates;
+
+};
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index a9960a1dd..ef3b0f01f 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -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 session)
, _globalPrivacy(std::make_unique(this))
, _userPrivacy(std::make_unique(this))
, _inviteLinks(std::make_unique(this))
+, _chatLinks(std::make_unique(this))
, _views(std::make_unique(this))
, _confirmPhone(std::make_unique(this))
, _peerPhoto(std::make_unique(this))
@@ -4424,6 +4426,10 @@ Api::InviteLinks &ApiWrap::inviteLinks() {
return *_inviteLinks;
}
+Api::ChatLinks &ApiWrap::chatLinks() {
+ return *_chatLinks;
+}
+
Api::ViewsManager &ApiWrap::views() {
return *_views;
}
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 58165adec..0d31126d3 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -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 _globalPrivacy;
const std::unique_ptr _userPrivacy;
const std::unique_ptr _inviteLinks;
+ const std::unique_ptr _chatLinks;
const std::unique_ptr _views;
const std::unique_ptr _confirmPhone;
const std::unique_ptr _peerPhoto;
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
index 5640c11b5..13f5b146d 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
@@ -580,8 +580,10 @@ void LinkController::addLinkBlock(not_null 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 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(
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
index fcdc646a5..7b80e2652 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
@@ -272,9 +272,10 @@ QImage QrForShare(const QString &text) {
void QrBox(
not_null box,
const QString &link,
+ rpl::producer title,
rpl::producer about,
Fn)> 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 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 show, const QString &link) {
object_ptr ShareInviteLinkBox(
not_null peer,
- const QString &link) {
- return ShareInviteLinkBox(&peer->session(), link);
+ const QString &link,
+ const QString &copied) {
+ return ShareInviteLinkBox(&peer->session(), link, copied);
}
object_ptr ShareInviteLinkBox(
not_null session,
- const QString &link) {
+ const QString &link,
+ const QString &copied) {
const auto sending = std::make_shared();
const auto box = std::make_shared>();
@@ -1148,7 +1154,9 @@ object_ptr 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> &&result,
@@ -1228,8 +1236,9 @@ object_ptr ShareInviteLinkBox(
object_ptr InviteLinkQrBox(
const QString &link,
+ rpl::producer title,
rpl::producer about) {
- return Box(QrBox, link, std::move(about), [=](
+ return Box(QrBox, link, std::move(title), std::move(about), [=](
const QImage &image,
std::shared_ptr show) {
auto mime = std::make_unique();
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h
index b4f54ef70..784bcc809 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h
@@ -41,12 +41,15 @@ void AddPermanentLinkBlock(
void CopyInviteLink(std::shared_ptr show, const QString &link);
[[nodiscard]] object_ptr ShareInviteLinkBox(
not_null peer,
- const QString &link);
+ const QString &link,
+ const QString &copied = {});
[[nodiscard]] object_ptr ShareInviteLinkBox(
not_null session,
- const QString &link);
+ const QString &link,
+ const QString &copied = {});
[[nodiscard]] object_ptr InviteLinkQrBox(
const QString &link,
+ rpl::producer title,
rpl::producer about);
[[nodiscard]] object_ptr RevokeLinkBox(
not_null peer,
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp
index 46905d67b..5952171ae 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp
@@ -214,38 +214,11 @@ object_ptr DeleteAllRevokedBox(
});
}
-not_null AddCreateLinkButton(
+[[nodiscard]] not_null AddCreateLinkButton(
not_null container) {
- const auto result = container->add(
- object_ptr(
- 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(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 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 MakeCreateLinkButton(
+ not_null parent,
+ rpl::producer text) {
+ auto result = object_ptr(
+ parent,
+ std::move(text),
+ st::inviteLinkCreate);
+ const auto raw = result.data();
+
+ const auto icon = Ui::CreateChild(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;
+}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h
index c16db91b4..3515b9655 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h
@@ -11,9 +11,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class PeerData;
+namespace Ui {
+class SettingsButton;
+} // namespace Ui
+
void ManageInviteLinksBox(
not_null box,
not_null peer,
not_null admin,
int count,
int revokedCount);
+
+[[nodiscard]] object_ptr MakeCreateLinkButton(
+ not_null parent,
+ rpl::producer text);
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
index 81404959c..bb71e2e63 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
@@ -146,6 +146,8 @@ void PreloadSticker(const std::shared_ptr &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 &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 "";
}();
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h
index fa520caaf..8e1dc503b 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.h
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.h
@@ -75,6 +75,7 @@ enum class PremiumFeature {
AwayMessage,
BusinessBots,
ChatIntro,
+ ChatLinks,
kCount,
};
diff --git a/Telegram/SourceFiles/settings/business/settings_chat_links.cpp b/Telegram/SourceFiles/settings/business/settings_chat_links.cpp
new file mode 100644
index 000000000..262b671f6
--- /dev/null
+++ b/Telegram/SourceFiles/settings/business/settings_chat_links.cpp
@@ -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
+
+namespace Settings {
+namespace {
+
+constexpr auto kChangesDebounceTimeout = crl::time(1000);
+
+using ChatLinkData = Api::ChatLink;
+
+class ChatLinks final : public BusinessSection {
+public:
+ ChatLinks(
+ QWidget *parent,
+ not_null controller);
+ ~ChatLinks();
+
+ [[nodiscard]] rpl::producer title() override;
+
+ const Ui::RoundRect *bottomSkipRounding() const override {
+ return &_bottomSkipRounding;
+ }
+
+private:
+ void setupContent(not_null 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 rowSession() = 0;
+ virtual void rowUpdateRow(not_null row) = 0;
+ virtual void rowPaintIcon(
+ QPainter &p,
+ int x,
+ int y,
+ int size) = 0;
+};
+
+class Row final : public PeerListRow {
+public:
+ Row(not_null 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 _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 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);
+
+ [[nodiscard]] rpl::producer fullCountValue() const {
+ return _count.value();
+ }
+
+ void prepare() override;
+ void rowClicked(not_null row) override;
+ void rowRightActionClicked(not_null row) override;
+ base::unique_qptr rowContextMenu(
+ QWidget *parent,
+ not_null row) override;
+ Main::Session &session() const override;
+
+ not_null rowSession() override;
+ void rowUpdateRow(not_null 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 row,
+ bool highlightRow);
+
+ [[nodiscard]] base::unique_qptr createRowContextMenu(
+ QWidget *parent,
+ not_null row);
+
+ const not_null _window;
+ const not_null _session;
+ rpl::variable _count;
+ base::unique_qptr _menu;
+
+ QImage _icon;
+ rpl::lifetime _lifetime;
+
+};
+
+struct LinksList {
+ not_null widget;
+ not_null controller;
+};
+
+LinksList AddLinksList(
+ not_null window,
+ not_null container) {
+ auto &lifetime = container->lifetime();
+ const auto delegate = lifetime.make_state(
+ window->uiShow());
+ const auto controller = lifetime.make_state(window);
+ controller->setStyleOverrides(&st::inviteLinkList);
+ const auto content = container->add(object_ptr(
+ container,
+ controller));
+ delegate->setContent(content);
+ controller->setDelegate(delegate);
+
+ return { content, controller };
+}
+
+void EditChatLinkBox(
+ not_null box,
+ not_null controller,
+ ChatLinkData data,
+ Fn 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(
+ 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(
+ box.get(),
+ st::defaultInputField,
+ tr::lng_chat_link_name(),
+ data.title));
+
+ const auto emojiToggle = Ui::CreateChild(
+ field->parentWidget(),
+ st::defaultComposeFiles.emoji);
+
+ using Selector = ChatHelpers::TabbedSelector;
+ auto &lifetime = box->lifetime();
+ const auto emojiPanel = lifetime.make_state(
+ outer,
+ controller,
+ object_ptr(
+ 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) { 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 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([=] {
+ 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,
+ not_null session,
+ ChatLinkData data) {
+ const auto submitting = std::make_shared();
+ const auto submit = [=](ChatLinkData data, Fn 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)
+: _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 row) {
+ showRowMenu(row, true);
+}
+
+void LinksController::showRowMenu(
+ not_null row,
+ bool highlightRow) {
+ delegate()->peerListShowRowMenu(row, highlightRow);
+}
+
+void LinksController::rowRightActionClicked(not_null row) {
+ delegate()->peerListShowRowMenu(row, true);
+}
+
+base::unique_qptr LinksController::rowContextMenu(
+ QWidget *parent,
+ not_null 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(result.get());
+ }
+
+ return result;
+}
+
+base::unique_qptr LinksController::createRowContextMenu(
+ QWidget *parent,
+ not_null row) {
+ const auto real = static_cast(row.get());
+ const auto data = real->data();
+ const auto link = data.link;
+ auto result = base::make_unique_q(
+ 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 &&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(this, data));
+ _count = _count.current() + 1;
+}
+
+void LinksController::prependRow(const ChatLinkData &data) {
+ delegate()->peerListPrependRow(std::make_unique(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);
+ 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 LinksController::rowSession() {
+ return _session;
+}
+
+void LinksController::rowUpdateRow(not_null 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 controller)
+: BusinessSection(parent, controller)
+, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
+ setupContent(controller);
+}
+
+ChatLinks::~ChatLinks() = default;
+
+rpl::producer ChatLinks::title() {
+ return tr::lng_chat_links_title();
+}
+
+void ChatLinks::setupContent(
+ not_null controller) {
+ using namespace rpl::mappers;
+
+ const auto content = Ui::CreateChild(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(
+ u"business_chat_links_limit"_q,
+ 100);
+ const auto add = content->add(
+ object_ptr>(
+ 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();
+ const auto submit = [=](ChatLinkData data, Fn 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 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
diff --git a/Telegram/SourceFiles/settings/business/settings_chat_links.h b/Telegram/SourceFiles/settings/business/settings_chat_links.h
new file mode 100644
index 000000000..ce4f010f8
--- /dev/null
+++ b/Telegram/SourceFiles/settings/business/settings_chat_links.h
@@ -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
diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp
index b8c990d03..57e74cf25 100644
--- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp
+++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp
@@ -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);
diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style
index 9ad3e3954..7c74c1ef2 100644
--- a/Telegram/SourceFiles/settings/settings.style
+++ b/Telegram/SourceFiles/settings/settings.style
@@ -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;
+}
diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp
index e6ae4de18..f0294643c 100644
--- a/Telegram/SourceFiles/settings/settings_business.cpp
+++ b/Telegram/SourceFiles/settings/settings_business.cpp
@@ -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 title;
rpl::producer description;
PremiumFeature feature = PremiumFeature::BusinessLocation;
+ bool newBadge = false;
};
using Order = std::vector;
@@ -70,7 +74,8 @@ using Order = std::vector;
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;
},
},
{
- 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(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 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) {