diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index eaa6f7f7e..ed5b9a818 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1144,6 +1144,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_info_mobile_label" = "Mobile"; "lng_info_mobile_hidden" = "Hidden"; "lng_info_username_label" = "Username"; +"lng_info_usernames_label" = "also"; "lng_info_bio_label" = "Bio"; "lng_info_link_label" = "Link"; "lng_info_location_label" = "Location"; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 87a0588b7..4c8b71a30 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -67,6 +67,7 @@ struct PeerUpdate { IsBlocked = (1ULL << 8), MessagesTTL = (1ULL << 9), FullInfo = (1ULL << 10), + Usernames = (1ULL << 11), // For users CanShareContact = (1ULL << 11), diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 28e9fe73d..8dc25bbbe 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -109,13 +109,18 @@ void ChannelData::setUsername(const QString &username) { } void ChannelData::setUsernames(const Data::Usernames &usernames) { - _usernames = ranges::views::all( + auto newUsernames = ranges::views::all( usernames ) | ranges::views::filter([&](const Data::Username &username) { return username.active; }) | ranges::views::transform([&](const Data::Username &username) { return username.username; }) | ranges::to_vector; + + if (!ranges::equal(_usernames, newUsernames)) { + _usernames = std::move(newUsernames); + session().changes().peerUpdated(this, UpdateFlag::Usernames); + } } QString ChannelData::username() const { diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index e8515a828..09c75553b 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -121,13 +121,18 @@ void UserData::setName(const QString &newFirstName, const QString &newLastName, } void UserData::setUsernames(const Data::Usernames &usernames) { - _usernames = ranges::views::all( + auto newUsernames = ranges::views::all( usernames ) | ranges::views::filter([&](const Data::Username &username) { return username.active; }) | ranges::views::transform([&](const Data::Username &username) { return username.username; }) | ranges::to_vector; + + if (!ranges::equal(_usernames, newUsernames)) { + _usernames = std::move(newUsernames); + session().changes().peerUpdated(this, UpdateFlag::Usernames); + } } void UserData::setUsername(const QString &username) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index acc5ace7b..33d9687a6 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/toast/toast.h" #include "ui/text/text_utilities.h" // Ui::Text::ToUpper +#include "ui/text/text_variant.h" #include "history/history_location_manager.h" // LocationClickHandler. #include "history/view/history_view_context_menu.h" // HistoryView::ShowReportPeerBox #include "boxes/abstract_box.h" @@ -67,12 +68,59 @@ namespace Info { namespace Profile { namespace { -object_ptr CreateSkipWidget( +[[nodiscard]] rpl::producer UsernamesSubtext( + not_null peer, + rpl::producer fallback) { + return rpl::combine( + UsernamesValue(peer), + std::move(fallback) + ) | rpl::map([](std::vector usernames, QString text) { + if (usernames.size() < 2) { + return TextWithEntities{ .text = text }; + } else { + auto result = TextWithEntities(); + result.append(tr::lng_info_usernames_label(tr::now)); + result.append(' '); + auto &&subrange = ranges::make_subrange( + begin(usernames) + 1, + end(usernames)); + for (auto &username : std::move(subrange)) { + const auto isLast = (usernames.back() == username); + result.append(Ui::Text::Link( + '@' + base::take(username.text), + username.entities.front().data())); + if (!isLast) { + result.append(u", "_q); + } + } + return result; + } + }); +} + +[[nodiscard]] Fn UsernamesLinkCallback( + not_null peer, + Window::Show show, + const QString &addToLink) { + return [=](QString link) { + if (!link.startsWith(u"https://"_q)) { + link = peer->session().createInternalLinkFull(peer->userName()); + } + if (!link.isEmpty()) { + QGuiApplication::clipboard()->setText(link + addToLink); + Ui::Toast::Show( + show.toastParent(), + tr::lng_username_copied(tr::now)); + } + }; +} + +[[nodiscard]] object_ptr CreateSkipWidget( not_null parent) { return Ui::CreateSkipWidget(parent, st::infoProfileSkip); } -object_ptr> CreateSlideSkipWidget( +[[nodiscard]] object_ptr> CreateSlideSkipWidget( not_null parent) { auto result = Ui::CreateSlideSkipWidget( parent, @@ -113,7 +161,7 @@ auto AddActionButton( }; template -auto AddMainButton( +[[nodiscard]] auto AddMainButton( not_null parent, Text &&text, ToggleOn &&toggleOn, @@ -263,23 +311,23 @@ object_ptr DetailsFiller::setupInfo() { }; const auto addInfoLineGeneric = [&]( - rpl::producer &&label, + v::text::data &&label, rpl::producer &&text, const style::FlatLabel &textSt = st::infoLabeled, const style::margins &padding = st::infoProfileLabeledPadding) { auto line = CreateTextWithLabel( result, - std::move(label) | Ui::Text::ToWithEntities(), + v::text::take_marked(std::move(label)), std::move(text), textSt, padding); tracker.track(result->add(std::move(line.wrap))); line.text->setClickHandlerFilter(infoClickFilter); - return line.text; + return line; }; const auto addInfoLine = [&]( - rpl::producer &&label, + v::text::data &&label, rpl::producer &&text, const style::FlatLabel &textSt = st::infoLabeled, const style::margins &padding = st::infoProfileLabeledPadding) { @@ -290,17 +338,17 @@ object_ptr DetailsFiller::setupInfo() { padding); }; const auto addInfoOneLine = [&]( - rpl::producer &&label, + v::text::data &&label, rpl::producer &&text, const QString &contextCopyText, const style::margins &padding = st::infoProfileLabeledPadding) { - const auto result = addInfoLine( + auto result = addInfoLine( std::move(label), std::move(text), st::infoLabeledOneLine, padding); - result->setDoubleClickSelectsParagraph(true); - result->setContextCopyText(contextCopyText); + result.text->setDoubleClickSelectsParagraph(true); + result.text->setContextCopyText(contextCopyText); return result; }; if (const auto user = _peer->asUser()) { @@ -320,11 +368,16 @@ object_ptr DetailsFiller::setupInfo() { : tr::lng_info_bio_label(); addInfoLine(std::move(label), AboutValue(user)); - const auto usernameLabel = addInfoOneLine( - tr::lng_info_username_label(), - UsernameValue(user), + const auto usernameLine = addInfoOneLine( + UsernamesSubtext(_peer, tr::lng_info_username_label()), + UsernameValue(user, true), tr::lng_context_copy_mention(tr::now), st::infoProfileLabeledUsernamePadding); + usernameLine.subtext->overrideLinkClickHandler(UsernamesLinkCallback( + _peer, + Window::Show(controller), + QString())); + const auto usernameLabel = usernameLine.text; if (user->isBot()) { const auto copyUsername = Ui::CreateChild( usernameLabel->parentWidget(), @@ -361,7 +414,8 @@ object_ptr DetailsFiller::setupInfo() { ? "?topic=" + QString::number(topicRootId.bare) : QString(); auto linkText = LinkValue( - _peer + _peer, + true ) | rpl::map([=](const QString &link) { return link.isEmpty() ? TextWithEntities() @@ -371,21 +425,17 @@ object_ptr DetailsFiller::setupInfo() { : link) + addToLink, link + addToLink); }); - auto link = addInfoOneLine( - tr::lng_info_link_label(), + auto linkLine = addInfoOneLine( + UsernamesSubtext(_peer, tr::lng_info_link_label()), std::move(linkText), QString()); const auto controller = _controller->parentController(); - link->overrideLinkClickHandler([=, peer = _peer] { - const auto link = peer->session().createInternalLinkFull( - peer->userName()); - if (!link.isEmpty()) { - QGuiApplication::clipboard()->setText(link + addToLink); - Ui::Toast::Show( - Window::Show(controller).toastParent(), - tr::lng_username_copied(tr::now)); - } - }); + const auto linkCallback = UsernamesLinkCallback( + _peer, + Window::Show(controller), + addToLink); + linkLine.text->overrideLinkClickHandler(linkCallback); + linkLine.subtext->overrideLinkClickHandler(linkCallback); if (const auto channel = _topic ? nullptr : _peer->asChannel()) { auto locationText = LocationValue( @@ -401,7 +451,7 @@ object_ptr DetailsFiller::setupInfo() { tr::lng_info_location_label(), std::move(locationText), QString() - )->setLinksTrusted(); + ).text->setLinksTrusted(); } addInfoLine( diff --git a/Telegram/SourceFiles/info/profile/info_profile_text.cpp b/Telegram/SourceFiles/info/profile/info_profile_text.cpp index d34773f05..d6655dc7e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_text.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_text.cpp @@ -51,7 +51,7 @@ TextWithLabel CreateTextWithLabel( textSt)); labeled->setSelectable(true); layout->add(Ui::CreateSkipWidget(layout, st::infoLabelSkip)); - layout->add(object_ptr( + const auto subtext = layout->add(object_ptr( layout, std::move( label @@ -60,7 +60,7 @@ TextWithLabel CreateTextWithLabel( }), st::infoLabel)); result->finishAnimating(); - return { std::move(result), labeled }; + return { std::move(result), labeled, subtext }; } } // namespace Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_text.h b/Telegram/SourceFiles/info/profile/info_profile_text.h index 813abd872..dbf8b8384 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_text.h +++ b/Telegram/SourceFiles/info/profile/info_profile_text.h @@ -28,6 +28,7 @@ namespace Profile { struct TextWithLabel { object_ptr> wrap; not_null text; + not_null subtext; }; TextWithLabel CreateTextWithLabel( diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 7bbe7e62c..19451cf03 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -47,14 +47,26 @@ auto PlainAboutValue(not_null peer) { } auto PlainUsernameValue(not_null peer) { - return peer->session().changes().peerFlagsValue( - peer, - UpdateFlag::Username + return rpl::merge( + peer->session().changes().peerFlagsValue(peer, UpdateFlag::Username), + peer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames) ) | rpl::map([=] { return peer->userName(); }); } +auto PlainPrimaryUsernameValue(not_null peer) { + return UsernamesValue( + peer + ) | rpl::map([=](std::vector usernames) { + if (!usernames.empty()) { + return rpl::single(usernames.front().text); + } else { + return PlainUsernameValue(peer); + } + }) | rpl::flatten_latest(); +} + void StripExternalLinks(TextWithEntities &text) { const auto local = [](const QString &url) { return !UrlRequiresConfirmation(QUrl::fromUserInput(url)); @@ -124,9 +136,12 @@ rpl::producer PhoneOrHiddenValue(not_null user) { }); } -rpl::producer UsernameValue(not_null user) { - return PlainUsernameValue( - user +rpl::producer UsernameValue( + not_null user, + bool primary) { + return (primary + ? PlainPrimaryUsernameValue(user) + : PlainUsernameValue(user) ) | rpl::map([](QString &&username) { return username.isEmpty() ? QString() @@ -134,6 +149,34 @@ rpl::producer UsernameValue(not_null user) { }) | Ui::Text::ToWithEntities(); } +rpl::producer> UsernamesValue( + not_null peer) { + const auto map = [=](const std::vector &usernames) { + return ranges::views::all( + usernames + ) | ranges::views::transform([&](const QString &u) { + return Ui::Text::Link( + u, + peer->session().createInternalLinkFull(u)); + }) | ranges::to_vector; + }; + auto value = rpl::merge( + peer->session().changes().peerFlagsValue(peer, UpdateFlag::Username), + peer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames) + ); + if (const auto user = peer->asUser()) { + return std::move(value) | rpl::map([=] { + return map(user->usernames()); + }); + } else if (const auto channel = peer->asChannel()) { + return std::move(value) | rpl::map([=] { + return map(channel->usernames()); + }); + } else { + return rpl::never>(); + } +} + TextWithEntities AboutWithEntities( not_null peer, const QString &value) { @@ -170,9 +213,10 @@ rpl::producer AboutValue(not_null peer) { }); } -rpl::producer LinkValue(not_null peer) { - return PlainUsernameValue( - peer +rpl::producer LinkValue(not_null peer, bool primary) { + return (primary + ? PlainPrimaryUsernameValue(peer) + : PlainUsernameValue(peer) ) | rpl::map([=](QString &&username) { return username.isEmpty() ? QString() diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index 98e26b83d..9291c99c2 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -54,13 +54,18 @@ rpl::producer> MigratedOrMeValue( [[nodiscard]] rpl::producer PhoneOrHiddenValue( not_null user); [[nodiscard]] rpl::producer UsernameValue( - not_null user); + not_null user, + bool primary = false); +[[nodiscard]] rpl::producer> UsernamesValue( + not_null peer); [[nodiscard]] TextWithEntities AboutWithEntities( not_null peer, const QString &value); [[nodiscard]] rpl::producer AboutValue( not_null peer); -[[nodiscard]] rpl::producer LinkValue(not_null peer); +[[nodiscard]] rpl::producer LinkValue( + not_null peer, + bool primary = false); [[nodiscard]] rpl::producer LocationValue( not_null channel); [[nodiscard]] rpl::producer NotificationsEnabledValue(