diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1fecca3543..9f0a2493b9 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 0000000000..21622df378 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 0000000000..3e7913e159 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 0000000000..6ef36c8502 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 0000000000..c9b5c3ea1a 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 3d1ac3b3c1..5cb49b2abe 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 12666b6fd3..b63b6d15f1 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 0000000000..f8a3f1979c --- /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 0000000000..34226eab94 --- /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 a9960a1ddb..ef3b0f01fc 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 58165adec4..0d31126d3c 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 5640c11b5a..13f5b146d4 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 fcdc646a5d..7b80e26528 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 b4f54ef709..784bcc809d 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 46905d67be..5952171aec 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 c16db91b4a..3515b96558 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 81404959c6..bb71e2e633 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 fa520caafe..8e1dc503b8 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 0000000000..262b671f6a --- /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 0000000000..ce4f010f82 --- /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 b8c990d03b..57e74cf25d 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 9ad3e39540..7c74c1ef2b 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 e6ae4de18b..f0294643cc 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) {