From 1061fb6c85c3056984781b889c458ff8322e48f8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Apr 2024 15:35:53 +0400 Subject: [PATCH] Implement collectible username / phone info boxes. --- .../animations/collectible_phone.tgs | Bin 0 -> 3228 bytes .../animations/collectible_username.tgs | Bin 0 -> 4492 bytes Telegram/Resources/langs/lang.strings | 8 + .../Resources/qrc/telegram/animations.qrc | 2 + Telegram/SourceFiles/apiwrap.cpp | 2 +- Telegram/SourceFiles/boxes/boxes.style | 30 ++ Telegram/SourceFiles/boxes/peer_list_box.cpp | 4 +- .../boxes/peers/edit_linked_chat_box.cpp | 2 +- .../boxes/peers/prepare_short_info_box.cpp | 2 +- .../SourceFiles/boxes/premium_limits_box.cpp | 6 +- .../SourceFiles/core/local_url_handlers.cpp | 36 +++ Telegram/SourceFiles/data/data_channel.cpp | 4 + Telegram/SourceFiles/data/data_channel.h | 1 + Telegram/SourceFiles/data/data_peer.cpp | 30 +- Telegram/SourceFiles/data/data_peer.h | 6 +- Telegram/SourceFiles/data/data_session.cpp | 2 +- .../data/data_sponsored_messages.cpp | 2 +- Telegram/SourceFiles/data/data_story.cpp | 2 +- Telegram/SourceFiles/data/data_user.cpp | 4 + Telegram/SourceFiles/data/data_user.h | 16 +- Telegram/SourceFiles/data/data_user_names.cpp | 6 + Telegram/SourceFiles/data/data_user_names.h | 1 + .../dialogs/dialogs_inner_widget.cpp | 4 +- .../admin_log/history_admin_log_item.cpp | 2 +- .../earn/info_earn_inner_widget.cpp | 24 +- .../earn/info_earn_inner_widget.h | 4 + .../info/profile/info_profile_actions.cpp | 28 +- .../info/profile/info_profile_phone_menu.cpp | 27 +- .../info/profile/info_profile_phone_menu.h | 2 + .../info/profile/info_profile_values.cpp | 33 ++- .../info/profile/info_profile_values.h | 3 + Telegram/SourceFiles/main/main_domain.cpp | 2 +- .../SourceFiles/settings/settings_main.cpp | 2 +- .../ui/boxes/collectible_info_box.cpp | 271 ++++++++++++++++++ .../ui/boxes/collectible_info_box.h | 45 +++ .../SourceFiles/ui/text/format_values.cpp | 1 + .../SourceFiles/window/window_main_menu.cpp | 4 +- .../window/window_session_controller.cpp | 77 +++++ .../window/window_session_controller.h | 8 + Telegram/cmake/td_ui.cmake | 2 + 40 files changed, 630 insertions(+), 75 deletions(-) create mode 100644 Telegram/Resources/animations/collectible_phone.tgs create mode 100644 Telegram/Resources/animations/collectible_username.tgs create mode 100644 Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp create mode 100644 Telegram/SourceFiles/ui/boxes/collectible_info_box.h diff --git a/Telegram/Resources/animations/collectible_phone.tgs b/Telegram/Resources/animations/collectible_phone.tgs new file mode 100644 index 0000000000000000000000000000000000000000..4e7284d5f5ab8c3630c4d68d78b3497e662d2946 GIT binary patch literal 3228 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 OsQ&@y>1%}=H~;|mPA<6s literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/collectible_username.tgs b/Telegram/Resources/animations/collectible_username.tgs new file mode 100644 index 0000000000000000000000000000000000000000..f93fd6d746df32fb00594bfe9e5eef882c6b92b2 GIT binary patch literal 4492 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d9e5dba0a..1dbe16fbb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -471,6 +471,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bio_placeholder" = "Bio"; +"lng_collectible_username_title" = "{username} is a collectible username that belongs to"; +"lng_collectible_username_info" = "This username was bought on **Fragment** on {date} for {price}"; +"lng_collectible_username_copy" = "Copy Link"; +"lng_collectible_phone_title" = "{phone} is a collectible phone number that belongs to"; +"lng_collectible_phone_info" = "This phone number was bought on **Fragment** on {date} for {price}"; +"lng_collectible_phone_copy" = "Copy Phone Number"; +"lng_collectible_learn_more" = "Learn More"; + "lng_settings_section_info" = "My info"; "lng_settings_section_notify" = "Notifications"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index b63b6d15f..5b72c461f 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -22,5 +22,7 @@ ../../animations/hours.tgs ../../animations/phone.tgs ../../animations/chat_link.tgs + ../../animations/collectible_username.tgs + ../../animations/collectible_phone.tgs diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index e53e7a5d7..b8919617d 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -796,7 +796,7 @@ QString ApiWrap::exportDirectStoryLink(not_null story) { const auto storyId = story->fullId(); const auto peer = story->peer(); const auto fallback = [&] { - const auto base = peer->userName(); + const auto base = peer->username(); const auto story = QString::number(storyId.story); const auto query = base + "/s/" + story; return session().createInternalLinkFull(query); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 2ae5bba8d..f64f2103d 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1044,3 +1044,33 @@ inviteForbiddenTitle: FlatLabel(boxTitle) { inviteForbiddenTitlePadding: margins(32px, 4px, 32px, 0px); inviteForbiddenLockBg: dialogsUnreadBgMuted; inviteForbiddenLockIcon: icon {{ "emoji/premium_lock", dialogsUnreadFg }}; + +collectibleIconDiameter: 72px; +collectibleIcon: 64px; +collectibleIconPadding: margins(24px, 32px, 24px, 12px); +collectibleHeader: FlatLabel(boxTitle) { + minWidth: 120px; + maxHeight: 0px; + align: align(top); +} +collectibleHeaderPadding: margins(24px, 16px, 24px, 12px); +collectibleOwnerPadding: margins(24px, 4px, 24px, 8px); +collectibleInfo: inviteForbiddenInfo; +collectibleInfoPadding: margins(24px, 12px, 24px, 12px); +collectibleInfoTonMargins: margins(0px, 3px, 0px, 0px); +collectibleMore: RoundButton(defaultActiveButton) { + height: 36px; + textTop: 9px; + radius: 6px; +} +collectibleMorePadding: margins(24px, 12px, 24px, 0px); +collectibleCopy: RoundButton(defaultLightButton) { + height: 36px; + textTop: 9px; + radius: 6px; +} +collectibleBox: Box(defaultBox) { + buttonPadding: margins(24px, 12px, 24px, 12px); + buttonHeight: 36px; + button: collectibleCopy; +} diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 2430c583e..149171e5b 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -1789,10 +1789,10 @@ crl::time PeerListContent::paintRow( if (row->isSearchResult() && !_mentionHighlight.isEmpty() && peer - && peer->userName().startsWith( + && peer->username().startsWith( _mentionHighlight, Qt::CaseInsensitive)) { - const auto username = peer->userName(); + const auto username = peer->username(); const auto availableWidth = statusw; auto highlightedPart = '@' + username.mid(0, _mentionHighlight.size()); auto grayedPart = username.mid(_mentionHighlight.size()); diff --git a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp index f8a7ea6e8..bae3d8a76 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -100,7 +100,7 @@ void Controller::prepare() { return; } auto row = std::make_unique(chat); - const auto username = chat->userName(); + const auto username = chat->username(); row->setCustomStatus(!username.isEmpty() ? ('@' + username) : (chat->isChannel() && !chat->isMegagroup()) diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index a6b442aca..6c19d4055 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -207,7 +207,7 @@ void ProcessFullPhoto( | UpdateFlag::Birthday) ) | rpl::map([=] { const auto user = peer->asUser(); - const auto username = peer->userName(); + const auto username = peer->username(); return PeerShortInfoFields{ .name = peer->name(), .phone = user ? Ui::FormatPhone(user->phone()) : QString(), diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 2a9c0d4b1..451957c21 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -322,7 +322,7 @@ void PublicsController::prepare() { auto &owner = _navigation->session().data(); for (const auto &chat : chats) { if (const auto peer = owner.processChat(chat)) { - if (!peer->isChannel() || peer->userName().isEmpty()) { + if (!peer->isChannel() || peer->username().isEmpty()) { continue; } appendRow(peer); @@ -346,7 +346,7 @@ void PublicsController::rowRightActionClicked(not_null row) { const auto text = textMethod( tr::now, lt_link, - peer->session().createInternalLink(peer->userName()), + peer->session().createInternalLink(peer->username()), lt_group, peer->name()); const auto confirmText = tr::lng_channels_too_much_public_revoke( @@ -389,7 +389,7 @@ std::unique_ptr PublicsController::createRow( auto result = std::make_unique(peer); result->setActionLink(tr::lng_channels_too_much_public_revoke(tr::now)); result->setCustomStatus( - _navigation->session().createInternalLink(peer->userName())); + _navigation->session().createInternalLink(peer->username())); return result; } diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 7ddee02d2..58c7af3a6 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -897,6 +897,34 @@ bool ShowEditPersonalChannel( return true; } +bool ShowCollectiblePhone( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto phone = match->captured(1); + const auto peerId = PeerId(match->captured(2).toULongLong()); + controller->resolveCollectible( + peerId, + phone.startsWith('+') ? phone : '+' + phone); + return true; +} + +bool ShowCollectibleUsername( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto username = match->captured(1); + const auto peerId = PeerId(match->captured(2).toULongLong()); + controller->resolveCollectible(peerId, username); + return true; +} + void ExportTestChatTheme( not_null controller, not_null theme) { @@ -1299,6 +1327,14 @@ const std::vector &InternalUrlHandlers() { u"^edit_personal_channel$"_q, ShowEditPersonalChannel, }, + { + u"^collectible_phone/([\\+0-9\\-\\s]+)@([0-9]+)$"_q, + ShowCollectiblePhone, + }, + { + u"^collectible_username/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q, + ShowCollectibleUsername, + }, }; return Result; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index b32c85c53..b088be2d7 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -139,6 +139,10 @@ const std::vector &ChannelData::usernames() const { return _username.usernames(); } +bool ChannelData::isUsernameEditable(QString username) const { + return _username.isEditable(username); +} + void ChannelData::setAccessHash(uint64 accessHash) { access = accessHash; input = MTP_inputPeerChannel( diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index d22352020..6dbe84329 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -182,6 +182,7 @@ public: [[nodiscard]] QString username() const; [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; + [[nodiscard]] bool isUsernameEditable(QString username) const; [[nodiscard]] int membersCount() const { return std::max(_membersCount, 1); diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 76389df4e..9b5d6d379 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -940,7 +940,7 @@ const QString &PeerData::shortName() const { return _name; } -QString PeerData::userName() const { +QString PeerData::username() const { if (const auto user = asUser()) { return user->username(); } else if (const auto channel = asChannel()) { @@ -949,6 +949,34 @@ QString PeerData::userName() const { return QString(); } +QString PeerData::editableUsername() const { + if (const auto user = asUser()) { + return user->editableUsername(); + } else if (const auto channel = asChannel()) { + return channel->editableUsername(); + } + return QString(); +} + +const std::vector &PeerData::usernames() const { + if (const auto user = asUser()) { + return user->usernames(); + } else if (const auto channel = asChannel()) { + return channel->usernames(); + } + static const auto kEmpty = std::vector(); + return kEmpty; +} + +bool PeerData::isUsernameEditable(QString username) const { + if (const auto user = asUser()) { + return user->isUsernameEditable(username); + } else if (const auto channel = asChannel()) { + return channel->isUsernameEditable(username); + } + return false; +} + bool PeerData::changeColorIndex(uint8 index) { index %= Ui::kColorIndexCount; if (_colorIndexCloud && _colorIndex == index) { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index bf7d4e025..b7f98e599 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -279,7 +279,11 @@ public: [[nodiscard]] const QString &name() const; [[nodiscard]] const QString &shortName() const; [[nodiscard]] const QString &topBarNameText() const; - [[nodiscard]] QString userName() const; + + [[nodiscard]] QString username() const; + [[nodiscard]] QString editableUsername() const; + [[nodiscard]] const std::vector &usernames() const; + [[nodiscard]] bool isUsernameEditable(QString username) const; [[nodiscard]] const base::flat_set &nameWords() const { return _nameWords; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 6bfd2b74e..4614fae09 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1230,7 +1230,7 @@ PeerData *Session::peerByUsername(const QString &username) const { const auto uname = username.trimmed(); for (const auto &[peerId, peer] : _peers) { if (peer->isLoaded() - && !peer->userName().compare(uname, Qt::CaseInsensitive)) { + && !peer->username().compare(uname, Qt::CaseInsensitive)) { return peer.get(); } } diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index a856e6156..22cfbfeef 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -303,7 +303,7 @@ void SponsoredMessages::append( ? _session->data().processBotApp(peerId, *data.vapp()) : nullptr; result.botLinkInfo = Window::PeerByLinkInfo{ - .usernameOrId = user->userName(), + .usernameOrId = user->username(), .resolveType = botAppData ? Window::ResolveType::BotApp : data.vstart_param() diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index d4b12de6a..cc0fefe99 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -450,7 +450,7 @@ bool Story::hasDirectLink() const { if (!_privacyPublic || (!_pinned && expired())) { return false; } - return !_peer->userName().isEmpty(); + return !_peer->username().isEmpty(); } std::optional Story::errorTextForForward( diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index cffe8ad67..f4f8faccd 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -472,6 +472,10 @@ const std::vector &UserData::usernames() const { return _username.usernames(); } +bool UserData::isUsernameEditable(QString username) const { + return _username.isEditable(username); +} + const QString &UserData::phone() const { return _phone; } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 7a8a22582..67da9e3f5 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -150,15 +150,11 @@ public: // a full check by canShareThisContact() call. [[nodiscard]] bool canShareThisContactFast() const; - MTPInputUser inputUser = MTP_inputUserEmpty(); - - QString firstName; - QString lastName; [[nodiscard]] const QString &phone() const; [[nodiscard]] QString username() const; [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; - QString nameOrPhone; + [[nodiscard]] bool isUsernameEditable(QString username) const; enum class ContactStatus : char { Unknown, @@ -186,8 +182,6 @@ public: void setBirthday(Data::Birthday value); void setBirthday(const tl::conditional &value); - std::unique_ptr botInfo; - void setUnavailableReasons( std::vector &&reasons); @@ -209,6 +203,14 @@ public: [[nodiscard]] MsgId personalChannelMessageId() const; void setPersonalChannel(ChannelId channelId, MsgId messageId); + MTPInputUser inputUser = MTP_inputUserEmpty(); + + QString firstName; + QString lastName; + QString nameOrPhone; + + std::unique_ptr botInfo; + private: auto unavailableReasons() const -> const std::vector & override; diff --git a/Telegram/SourceFiles/data/data_user_names.cpp b/Telegram/SourceFiles/data/data_user_names.cpp index b69756b5e..c2edd6b01 100644 --- a/Telegram/SourceFiles/data/data_user_names.cpp +++ b/Telegram/SourceFiles/data/data_user_names.cpp @@ -80,4 +80,10 @@ const std::vector &UsernamesInfo::usernames() const { return _usernames; } +bool UsernamesInfo::isEditable(const QString &username) const { + return (_indexEditableUsername >= 0) + && (_indexEditableUsername < _usernames.size()) + && (_usernames[_indexEditableUsername] == username); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_user_names.h b/Telegram/SourceFiles/data/data_user_names.h index 03853b520..04912ced9 100644 --- a/Telegram/SourceFiles/data/data_user_names.h +++ b/Telegram/SourceFiles/data/data_user_names.h @@ -27,6 +27,7 @@ public: [[nodiscard]] QString username() const; [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; + [[nodiscard]] bool isEditable(const QString &username) const; private: std::vector _usernames; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 354a148eb..0da825209 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1087,7 +1087,7 @@ void InnerWidget::paintPeerSearchResult( QRect tr(context.st->textLeft, context.st->textTop, namewidth, st::dialogsTextFont->height); p.setFont(st::dialogsTextFont); - QString username = peer->userName(); + QString username = peer->username(); if (!context.active && username.startsWith(_peerSearchQuery, Qt::CaseInsensitive)) { auto first = '@' + username.mid(0, _peerSearchQuery.size()); auto second = username.mid(_peerSearchQuery.size()); @@ -4021,7 +4021,7 @@ void InnerWidget::setupShortcuts() { const auto history = thread->owningHistory(); const auto isArchived = history->folder() && (history->folder()->id() == Data::Folder::kId); - + Window::ToggleHistoryArchived( _controller->uiShow(), history, diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index ab1f19958..5899117be 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -496,7 +496,7 @@ auto GenerateParticipantString( data, }); } - const auto username = peer->userName(); + const auto username = peer->username(); if (username.isEmpty()) { return name; } diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp index 1a0cedb1b..494f58f9a 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp @@ -217,7 +217,18 @@ void AddRecipient(not_null box, const TextWithEntities &t) { } #endif -[[nodiscard]] QImage IconCurrency( +[[nodiscard]] QString FormatDate(const QDateTime &date) { + return tr::lng_group_call_starts_short_date( + tr::now, + lt_date, + langDayOfMonth(date.date()), + lt_time, + QLocale().toString(date.time(), QLocale::ShortFormat)); +} + +} // namespace + +QImage IconCurrency( const style::FlatLabel &label, const QColor &c) { const auto s = Size(label.style.font->ascent); @@ -234,17 +245,6 @@ void AddRecipient(not_null box, const TextWithEntities &t) { return image; } -[[nodiscard]] QString FormatDate(const QDateTime &date) { - return tr::lng_group_call_starts_short_date( - tr::now, - lt_date, - langDayOfMonth(date.date()), - lt_time, - QLocale().toString(date.time(), QLocale::ShortFormat)); -} - -} // namespace - InnerWidget::InnerWidget( QWidget *parent, not_null controller, diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h index 69d1587d8..99e9511be 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h @@ -23,6 +23,10 @@ namespace Info::ChannelEarn { class Memento; +[[nodiscard]] QImage IconCurrency( + const style::FlatLabel &label, + const QColor &c); + class InnerWidget final : public Ui::VerticalLayout { public: struct ShowRequest final { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 19d7eebae..84d8daba3 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -116,16 +116,25 @@ base::options::toggle ShowPeerIdBelowAbout({ [[nodiscard]] Fn UsernamesLinkCallback( not_null peer, - std::shared_ptr show, + not_null controller, const QString &addToLink) { + const auto weak = base::make_weak(controller); return [=](QString link) { - if (!link.startsWith(u"https://"_q)) { - link = peer->session().createInternalLinkFull(peer->userName()) + if (link.startsWith(u"internal:"_q)) { + Core::App().openInternalUrl(link, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + })); + return; + } else if (!link.startsWith(u"https://"_q)) { + link = peer->session().createInternalLinkFull(peer->username()) + addToLink; } if (!link.isEmpty()) { QGuiApplication::clipboard()->setText(link); - show->showToast(tr::lng_username_copied(tr::now)); + if (const auto window = weak.get()) { + window->showToast(tr::lng_username_copied(tr::now)); + } } }; } @@ -1041,16 +1050,13 @@ object_ptr DetailsFiller::setupInfo() { UsernameValue(user, true) | rpl::map([=](TextWithEntities u) { return u.text.isEmpty() ? TextWithEntities() - : Ui::Text::Link( - u, - user->session().createInternalLinkFull( - u.text.mid(1))); + : Ui::Text::Link(u, UsernameUrl(user, u.text.mid(1))); }), QString(), st::infoProfileLabeledUsernamePadding); const auto callback = UsernamesLinkCallback( _peer, - controller->uiShow(), + controller, QString()); const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) { if (!request.link) { @@ -1094,7 +1100,7 @@ object_ptr DetailsFiller::setupInfo() { }, copyUsername->lifetime()); copyUsername->setClickedCallback([=] { const auto link = user->session().createInternalLinkFull( - user->userName()); + user->username()); if (!link.isEmpty()) { QGuiApplication::clipboard()->setText(link); controller->showToast(tr::lng_username_copied(tr::now)); @@ -1159,7 +1165,7 @@ object_ptr DetailsFiller::setupInfo() { const auto controller = _controller->parentController(); const auto linkCallback = UsernamesLinkCallback( _peer, - controller->uiShow(), + controller, addToLink); linkLine.text->overrideLinkClickHandler(linkCallback); linkLine.subtext->overrideLinkClickHandler(linkCallback); diff --git a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp index 58c51ed31..85fde827f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp @@ -112,23 +112,22 @@ int TextItem::contentHeight() const { } // namespace -void AddPhoneMenu(not_null menu, not_null user) { - if (user->isSelf()) { - return; - } +bool IsCollectiblePhone(not_null user) { using Strings = std::vector; const auto prefixes = user->session().appConfig().get( u"fragment_prefixes"_q, - std::vector()); - { - const auto proj = [&phone = user->phone()](const QString &p) { - return phone.startsWith(p); - }; - if (ranges::none_of(prefixes, proj)) { - return; - } - } - if (const auto url = AppConfig::FragmentLink(&user->session())) { + Strings{ u"888"_q }); + const auto phone = user->phone(); + const auto proj = [&](const QString &p) { + return phone.startsWith(p); + }; + return ranges::any_of(prefixes, proj); +} + +void AddPhoneMenu(not_null menu, not_null user) { + if (user->isSelf() || !IsCollectiblePhone(user)) { + return; + } else if (const auto url = AppConfig::FragmentLink(&user->session())) { menu->addSeparator(&st::expandedMenuSeparator); const auto link = Ui::Text::Link( tr::lng_info_mobile_context_menu_fragment_about_link(tr::now), diff --git a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h index 82ed6bb8c..25e6c8b5c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h +++ b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h @@ -16,6 +16,8 @@ class PopupMenu; namespace Info { namespace Profile { +[[nodiscard]] bool IsCollectiblePhone(not_null user); + void AddPhoneMenu(not_null menu, not_null user); } // namespace Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 9fe23da1f..6159fe907 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "apiwrap.h" +#include "info/profile/info_profile_phone_menu.h" #include "info/profile/info_profile_badge.h" #include "core/application.h" #include "core/click_handler_types.h" @@ -55,7 +56,7 @@ auto PlainUsernameValue(not_null peer) { peer->session().changes().peerFlagsValue(peer, UpdateFlag::Username), peer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames) ) | rpl::map([=] { - return peer->userName(); + return peer->username(); }); } @@ -136,14 +137,19 @@ rpl::producer PhoneOrHiddenValue(not_null user) { PlainUsernameValue(user), PlainAboutValue(user), tr::lng_info_mobile_hidden() - ) | rpl::map([]( + ) | rpl::map([user]( const TextWithEntities &phone, const QString &username, const QString &about, const QString &hidden) { - return (phone.text.isEmpty() && username.isEmpty() && about.isEmpty()) - ? Ui::Text::WithEntities(hidden) - : phone; + if (phone.text.isEmpty() && username.isEmpty() && about.isEmpty()) { + return Ui::Text::WithEntities(hidden); + } else if (IsCollectiblePhone(user)) { + return Ui::Text::Link(phone, u"internal:collectible_phone/"_q + + user->phone() + '@' + QString::number(user->id.value)); + } else { + return phone; + } }); } @@ -160,15 +166,22 @@ rpl::producer UsernameValue( }) | Ui::Text::ToWithEntities(); } +QString UsernameUrl(not_null peer, const QString &username) { + return peer->isUsernameEditable(username) + ? peer->session().createInternalLinkFull(username) + : (u"internal:collectible_username/"_q + + username + + "@" + + QString::number(peer->id.value)); +} + 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)); + return Ui::Text::Link(u, UsernameUrl(peer, u)); }) | ranges::to_vector; }; auto value = rpl::merge( @@ -224,9 +237,7 @@ rpl::producer LinkValue(not_null peer, bool primary) { ? PlainPrimaryUsernameValue(peer) : PlainUsernameValue(peer) | rpl::type_erased() ) | rpl::map([=](QString &&username) { - return username.isEmpty() - ? QString() - : peer->session().createInternalLinkFull(username); + return username.isEmpty() ? QString() : UsernameUrl(peer, username); }); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index 6b7ccf475..625ccfa2a 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -61,6 +61,9 @@ rpl::producer> MigratedOrMeValue( bool primary = false); [[nodiscard]] rpl::producer> UsernamesValue( not_null peer); +[[nodiscard]] QString UsernameUrl( + not_null peer, + const QString &username); [[nodiscard]] TextWithEntities AboutWithEntities( not_null peer, const QString &value); diff --git a/Telegram/SourceFiles/main/main_domain.cpp b/Telegram/SourceFiles/main/main_domain.cpp index cc6bd9457..aafbcd2ac 100644 --- a/Telegram/SourceFiles/main/main_domain.cpp +++ b/Telegram/SourceFiles/main/main_domain.cpp @@ -53,7 +53,7 @@ Domain::Domain(const QString &dataName) : rpl::never(); }) | rpl::flatten_latest( ) | rpl::start_with_next([](const Data::PeerUpdate &update) { - CrashReports::SetAnnotation("Username", update.peer->userName()); + CrashReports::SetAnnotation("Username", update.peer->username()); }, _lifetime); } diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index ffa288223..896a38a6b 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -224,7 +224,7 @@ void Cover::initViewers() { }, lifetime()); _username->overrideLinkClickHandler([=] { - const auto username = _user->userName(); + const auto username = _user->username(); if (username.isEmpty()) { _controller->show(Box(UsernamesBox, _user)); } else { diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp new file mode 100644 index 000000000..4498becad --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp @@ -0,0 +1,271 @@ +/* +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 "ui/boxes/collectible_info_box.h" + +#include "base/unixtime.h" +#include "core/file_utilities.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "info/channel_statistics/earn/earn_format.h" +#include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/dynamic_image.h" +#include "ui/painter.h" +#include "settings/settings_common.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +#include +#include + +namespace Ui { +namespace { + +constexpr auto kTonMultiplier = uint64(1000000000); + +[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) { + switch (type) { + case CollectibleType::Phone: { + static const auto kNonDigits = QRegularExpression(u"[^\\d]"_q); + entity.replace(kNonDigits, QString()); + } return Ui::FormatPhone(entity); + case CollectibleType::Username: + return entity.startsWith('@') ? entity : ('@' + entity); + } + Unexpected("CollectibleType in FormatEntity."); +} + +[[nodiscard]] QString FormatDate(TimeId date) { + return langDateTime(base::unixtime::parse(date)); +} + +[[nodiscard]] TextWithEntities FormatPrice( + const CollectibleInfo &info, + const CollectibleDetails &details) { + auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount); + if (minor.size() == 1 && minor.at(0) == '.') { + minor += '0'; + } + auto price = (info.cryptoCurrency == u"TON"_q) + ? base::duplicate( + details.tonEmoji + ).append( + Info::ChannelEarn::MajorPart(info.cryptoAmount) + ).append(minor) + : TextWithEntities{ ('{' + + info.cryptoCurrency + ':' + QString::number(info.cryptoAmount) + + '}') }; + const auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency); + return Ui::Text::Wrapped( + price, + EntityType::Bold + ).append(u" ("_q + fiat + ')'); +} + +[[nodiscard]] object_ptr MakeOwnerCell( + not_null parent, + const CollectibleInfo &info) { + const auto st = &st::defaultMultiSelectItem; + const auto size = st->height; + auto result = object_ptr(parent.get(), size); + const auto raw = result.data(); + + const auto name = info.ownerName; + const auto userpic = info.ownerUserpic; + const auto nameWidth = st->style.font->width(name); + const auto added = size + st->padding.left() + st->padding.right(); + const auto subscribed = std::make_shared(false); + raw->paintRequest() | rpl::start_with_next([=] { + const auto use = std::min(nameWidth + added, raw->width()); + const auto x = (raw->width() - use) / 2; + if (const auto available = use - added; available > 0) { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st->textBg); + p.drawRoundedRect(x, 0, use, size, size / 2., size / 2.); + + if (!*subscribed) { + *subscribed = true; + userpic->subscribeToUpdates([=] { raw->update(); }); + } + p.drawImage(QRect(x, 0, size, size), userpic->image(size)); + + const auto textx = x + size + st->padding.left(); + const auto texty = st->padding.top() + st->style.font->ascent; + const auto text = (use == nameWidth + added) + ? name + : st->style.font->elided(name, available); + p.setPen(st->textFg); + p.setFont(st->style.font); + p.drawText(textx, texty, text); + } + }, raw->lifetime()); + + return result; +} + +} // namespace + +CollectibleType DetectCollectibleType(const QString &entity) { + return entity.startsWith('+') + ? CollectibleType::Phone + : CollectibleType::Username; +} + +void CollectibleInfoBox( + not_null box, + CollectibleInfo info, + CollectibleDetails details) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::collectibleBox); + + const auto type = DetectCollectibleType(info.entity); + + const auto icon = box->addRow( + object_ptr(box, st::collectibleIconDiameter), + st::collectibleIconPadding); + icon->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + const auto size = icon->height(); + const auto inner = QRect( + (icon->width() - size) / 2, + 0, + size, + size); + if (!inner.intersects(clip)) { + return; + } + auto p = QPainter(icon); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::defaultActiveButton.textBg); + p.setPen(Qt::NoPen); + p.drawEllipse(inner); + }, icon->lifetime()); + const auto lottieSize = st::collectibleIcon; + auto lottie = Settings::CreateLottieIcon( + icon, + { + .name = (type == CollectibleType::Phone + ? u"collectible_phone"_q + : u"collectible_username"_q), + .color = &st::defaultActiveButton.textFg, + .sizeOverride = { lottieSize, lottieSize }, + }, + QMargins()); + box->showFinishes( + ) | rpl::start_with_next([animate = std::move(lottie.animate)] { + animate(anim::repeat::once); + }, box->lifetime()); + const auto animation = lottie.widget.release(); + icon->sizeValue() | rpl::start_with_next([=](QSize size) { + const auto skip = (type == CollectibleType::Phone) + ? style::ConvertScale(2) + : 0; + animation->move( + (size.width() - animation->width()) / 2, + skip + (size.height() - animation->height()) / 2); + }, animation->lifetime()); + + const auto formatted = FormatEntity(type, info.entity); + const auto header = (type == CollectibleType::Phone) + ? tr::lng_collectible_phone_title( + tr::now, + lt_phone, + Ui::Text::Link(formatted), + Ui::Text::WithEntities) + : tr::lng_collectible_username_title( + tr::now, + lt_username, + Ui::Text::Link(formatted), + Ui::Text::WithEntities); + const auto copyCallback = [box, type, formatted, text = info.copyText] { + QGuiApplication::clipboard()->setText( + text.isEmpty() ? formatted : text); + box->uiShow()->showToast((type == CollectibleType::Phone) + ? tr::lng_text_copied(tr::now) + : tr::lng_username_copied(tr::now)); + }; + box->addRow( + object_ptr( + box, + rpl::single(header), + st::collectibleHeader), + st::collectibleHeaderPadding + )->setClickHandlerFilter([copyCallback](const auto &...) { + copyCallback(); + return false; + }); + + box->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding); + + const auto text = ((type == CollectibleType::Phone) + ? tr::lng_collectible_phone_info + : tr::lng_collectible_username_info)( + tr::now, + lt_date, + TextWithEntities{ FormatDate(info.date) }, + lt_price, + FormatPrice(info, details), + Ui::Text::RichLangValue); + const auto label = box->addRow( + object_ptr(box, st::collectibleInfo), + st::collectibleInfoPadding); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setMarkedText(text, details.tonEmojiContext()); + + const auto more = box->addRow( + object_ptr( + box, + tr::lng_collectible_learn_more(), + st::collectibleMore), + st::collectibleMorePadding); + more->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + more->setClickedCallback([url = info.url] { + File::OpenUrl(url); + }); + + const auto phrase = (type == CollectibleType::Phone) + ? tr::lng_collectible_phone_copy + : tr::lng_collectible_username_copy; + auto owned = object_ptr( + box, + phrase(), + st::collectibleCopy); + const auto copy = owned.data(); + copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + copy->setClickedCallback(copyCallback); + box->addButton(std::move(owned)); + + box->setNoContentMargin(true); + const auto buttonsParent = box->verticalLayout().get(); + const auto close = Ui::CreateChild( + buttonsParent, + st::boxTitleClose); + close->setClickedCallback([=] { + box->closeBox(); + }); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0); + }, box->lifetime()); + + box->widthValue() | rpl::start_with_next([=](int width) { + more->setFullWidth(width + - st::collectibleMorePadding.left() + - st::collectibleMorePadding.right()); + copy->setFullWidth(width + - st::collectibleBox.buttonPadding.left() + - st::collectibleBox.buttonPadding.right()); + }, box->lifetime()); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.h b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h new file mode 100644 index 000000000..8b80b9650 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h @@ -0,0 +1,45 @@ +/* +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 + +namespace Ui { + +class GenericBox; +class DynamicImage; + +enum class CollectibleType { + Phone, + Username, +}; + +[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity); + +struct CollectibleInfo { + QString entity; + QString copyText; + std::shared_ptr ownerUserpic; + QString ownerName; + uint64 cryptoAmount = 0; + uint64 amount = 0; + QString cryptoCurrency; + QString currency; + QString url; + TimeId date = 0; +}; + +struct CollectibleDetails { + TextWithEntities tonEmoji; + Fn tonEmojiContext; +}; + +void CollectibleInfoBox( + not_null box, + CollectibleInfo info, + CollectibleDetails details); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index dec5eb312..41e985723 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -357,6 +357,7 @@ CurrencyRule LookupCurrencyRule(const QString ¤cy) { char do_decimal_point() const override { return decimal; } char do_thousands_sep() const override { return thousands; } + std::string do_grouping() const override { return "\3"; } char decimal = '.'; char thousands = ','; diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 0276c3771..d5c2d3fce 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -135,8 +135,8 @@ not_null AddMyChannelsBox( const auto count = c ? c->membersCount() : g->count; _status.setText( st::defaultTextStyle, - !p->userName().isEmpty() - ? ('@' + p->userName()) + !p->username().isEmpty() + ? ('@' + p->username()) : count ? tr::lng_chat_status_subscribers( tr::now, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 18c536cf3..86a5f53c1 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/delete_messages_box.h" #include "window/window_controller.h" #include "window/window_filters_menu.h" +#include "info/channel_statistics/earn/info_earn_inner_widget.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "inline_bots/bot_attach_web_view.h" @@ -26,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_scheduled_section.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_document_resolver.h" #include "data/data_download_manager.h" #include "data/data_session.h" @@ -49,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/shortcuts.h" #include "core/application.h" #include "core/click_handler_types.h" +#include "core/ui_integration.h" #include "base/unixtime.h" #include "ui/controls/userpic_button.h" #include "ui/text/text_utilities.h" @@ -62,7 +65,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_instance.h" // Core::App().calls().inCall(). #include "calls/group/calls_group_call.h" #include "ui/boxes/calendar_box.h" +#include "ui/boxes/collectible_info_box.h" #include "ui/boxes/confirm_box.h" +#include "ui/dynamic_thumbnails.h" #include "mainwidget.h" #include "main/main_app_config.h" #include "main/main_domain.h" @@ -83,6 +88,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "styles/style_window.h" +#include "styles/style_boxes.h" #include "styles/style_dialogs.h" #include "styles/style_layers.h" // st::boxLabel @@ -155,6 +161,47 @@ private: return false; } +[[nodiscard]] Ui::CollectibleDetails PrepareCollectibleDetails( + not_null session) { + const auto makeContext = [=] { + return Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + }; + }; + return { + .tonEmoji = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + Info::ChannelEarn::IconCurrency( + st::collectibleInfo, + st::collectibleInfo.textFg->c), + st::collectibleInfoTonMargins, + true)), + .tonEmojiContext = makeContext, + }; +} + +[[nodiscard]] Ui::CollectibleInfo Parse( + const QString &entity, + not_null owner, + const MTPfragment_CollectibleInfo &info) { + const auto &data = info.data(); + return { + .entity = entity, + .copyText = (entity.startsWith('+') + ? QString() + : owner->session().createInternalLinkFull(entity)), + .ownerUserpic = Ui::MakeUserpicThumbnail(owner, true), + .ownerName = owner->name(), + .cryptoAmount = data.vcrypto_amount().v, + .amount = data.vamount().v, + .cryptoCurrency = qs(data.vcrypto_currency()), + .currency = qs(data.vcurrency()), + .url = qs(data.vurl()), + .date = data.vpurchase_date().v, + }; +} + MainWindowShow::MainWindowShow(not_null controller) : _window(base::make_weak(controller)) { } @@ -688,6 +735,36 @@ void SessionNavigation::resolveBoostState(not_null channel) { }).send(); } +void SessionNavigation::resolveCollectible( + PeerId ownerId, + const QString &entity, + Fn fail) { + if (_collectibleEntity == entity) { + return; + } else { + _api.request(base::take(_collectibleRequestId)).cancel(); + } + _collectibleEntity = entity; + _collectibleRequestId = _api.request(MTPfragment_GetCollectibleInfo( + ((Ui::DetectCollectibleType(entity) == Ui::CollectibleType::Phone) + ? MTP_inputCollectiblePhone(MTP_string(entity)) + : MTP_inputCollectibleUsername(MTP_string(entity))) + )).done([=](const MTPfragment_CollectibleInfo &result) { + const auto entity = base::take(_collectibleEntity); + _collectibleRequestId = 0; + uiShow()->show(Box( + Ui::CollectibleInfoBox, + Parse(entity, _session->data().peer(ownerId), result), + PrepareCollectibleDetails(_session))); + }).fail([=](const MTP::Error &error) { + _collectibleEntity = QString(); + _collectibleRequestId = 0; + if (fail) { + fail(error.type()); + } + }).send(); +} + void SessionNavigation::applyBoost( not_null channel, Fn done) { diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 811fb87a3..293dda20c 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -245,6 +245,11 @@ public: void resolveBoostState(not_null channel); + void resolveCollectible( + PeerId ownerId, + const QString &entity, + Fn fail = nullptr); + base::weak_ptr showToast( Ui::Toast::Config &&config); base::weak_ptr showToast( @@ -304,6 +309,9 @@ private: ChannelData *_boostStateResolving = nullptr; + QString _collectibleEntity; + mtpRequestId _collectibleRequestId = 0; + }; class SessionController : public SessionNavigation { diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 3cd0defa7..3a8f8ef34 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -240,6 +240,8 @@ PRIVATE ui/boxes/choose_language_box.h ui/boxes/choose_time.cpp ui/boxes/choose_time.h + ui/boxes/collectible_info_box.cpp + ui/boxes/collectible_info_box.h ui/boxes/confirm_box.cpp ui/boxes/confirm_box.h ui/boxes/confirm_phone_box.cpp