From 3d54f8ec4903ebb519368ad7919571ead86d84e6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 26 Mar 2024 14:00:34 +0400 Subject: [PATCH] Initial chat links edition implementation. --- Telegram/CMakeLists.txt | 4 + Telegram/Resources/animations/chat_link.tgs | Bin 0 -> 28567 bytes .../icons/settings/premium/links.png | Bin 0 -> 610 bytes .../icons/settings/premium/links@2x.png | Bin 0 -> 1253 bytes .../icons/settings/premium/links@3x.png | Bin 0 -> 2121 bytes Telegram/Resources/langs/lang.strings | 30 +- .../Resources/qrc/telegram/animations.qrc | 1 + Telegram/SourceFiles/api/api_chat_links.cpp | 171 ++++ Telegram/SourceFiles/api/api_chat_links.h | 64 ++ Telegram/SourceFiles/apiwrap.cpp | 6 + Telegram/SourceFiles/apiwrap.h | 3 + .../boxes/filters/edit_filter_links.cpp | 12 +- .../boxes/peers/edit_peer_invite_link.cpp | 25 +- .../boxes/peers/edit_peer_invite_link.h | 7 +- .../boxes/peers/edit_peer_invite_links.cpp | 78 +- .../boxes/peers/edit_peer_invite_links.h | 8 + .../SourceFiles/boxes/premium_preview_box.cpp | 5 + .../SourceFiles/boxes/premium_preview_box.h | 1 + .../settings/business/settings_chat_links.cpp | 813 ++++++++++++++++++ .../settings/business/settings_chat_links.h | 16 + .../business/settings_quick_replies.cpp | 6 +- Telegram/SourceFiles/settings/settings.style | 21 + .../settings/settings_business.cpp | 32 +- 23 files changed, 1250 insertions(+), 53 deletions(-) create mode 100644 Telegram/Resources/animations/chat_link.tgs create mode 100644 Telegram/Resources/icons/settings/premium/links.png create mode 100644 Telegram/Resources/icons/settings/premium/links@2x.png create mode 100644 Telegram/Resources/icons/settings/premium/links@3x.png create mode 100644 Telegram/SourceFiles/api/api_chat_links.cpp create mode 100644 Telegram/SourceFiles/api/api_chat_links.h create mode 100644 Telegram/SourceFiles/settings/business/settings_chat_links.cpp create mode 100644 Telegram/SourceFiles/settings/business/settings_chat_links.h 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 0000000000000000000000000000000000000000..21622df3788cb21a08f7748382aad65caaf72652 GIT binary patch literal 28567 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6ef36c850231f3779fcd6f3a0d8d5ecd8363b250 GIT binary patch literal 1253 zcmVsg%p*85tP>@cjIIeSK}S*?c}<{HAd{_GM*dTCMi%>{LT`7J;^9#5y!aYrFbr_+5gkVqslnN0D~5HgueB9Z88 zfFu%$!{Ojq7)4Qw#bP#_olYlZ$l-8EB$6)xvRbWNP%sQ@YilDjfZ^d`N?fT_G66Ae9nHE~=ej<5N>p>+9<{j&E&k5k3_xB?fWqa(cR%=yN6*;o9vO=_nlBv;XR4Uc-^78%tz1Qn4 zE-vor>5)pMOwNQk;}}zinwpv@m(|zT2h6+M`T6jeNFk9Tx*M2>H7Zxi_jgMmwX zdV7201OfmALC9X=!^7CvnA7PzJv|*78X_Wky`Bq=-ELe)p9x%!4_14wZMJ*J=Yfx5JmdE3Xj|M@IqM{-$RbqQlTwGj? z73J~qQKQjt^au8pny##@Ebhq&f-EjBE-fv&-EQu)fcwV=sZ?59TT6}@3RDy z-$4T$e?#f%>2Gguv$L~LPfr$$g*cdgqy9b%N6{ZpH2pfDUnkyg`+Mp?&Fj9U1ofkf P00000NkvXXu0mjfRfays literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c9b5c3ea1a1b83bcae023a418b06eadd5b09ff51 GIT binary patch literal 2121 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 literal 0 HcmV?d00001 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) {