diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index eceb747c8a..c7671635da 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -446,6 +446,8 @@ PRIVATE chat_helpers/ttl_media_layer_widget.h core/application.cpp core/application.h + core/bank_card_click_handler.cpp + core/bank_card_click_handler.h core/base_integration.cpp core/base_integration.h core/changelogs.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7ffa331a5e..983e6da4a1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6440,6 +6440,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted."; "lng_frozen_appeal_button" = "Submit an Appeal"; +"lng_context_bank_card_copy" = "Copy Card Number"; +"lng_context_bank_card_copied" = "Card number copied to clipboard."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index cfafeb3647..9f400d05cc 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -202,7 +202,11 @@ EntitiesInText EntitiesFromMTP( d.vlength().v, }); }, [&](const MTPDmessageEntityBankCard &d) { - // Skipping cards. // #TODO entities + result.push_back({ + EntityType::BankCard, + d.voffset().v, + d.vlength().v, + }); }, [&](const MTPDmessageEntitySpoiler &d) { result.push_back({ EntityType::Spoiler, @@ -273,6 +277,9 @@ MTPVector EntitiesToMTP( case EntityType::Phone: { v.push_back(MTP_messageEntityPhone(offset, length)); } break; + case EntityType::BankCard: { + v.push_back(MTP_messageEntityBankCard(offset, length)); + } break; case EntityType::Hashtag: { v.push_back(MTP_messageEntityHashtag(offset, length)); } break; diff --git a/Telegram/SourceFiles/core/bank_card_click_handler.cpp b/Telegram/SourceFiles/core/bank_card_click_handler.cpp new file mode 100644 index 0000000000..51d25663ec --- /dev/null +++ b/Telegram/SourceFiles/core/bank_card_click_handler.cpp @@ -0,0 +1,254 @@ +/* +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 "core/bank_card_click_handler.h" + +#include "core/click_handler_types.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "mtproto/sender.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/menu/menu_multiline_action.h" +#include "ui/widgets/popup_menu.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" // popupMenuExpandedSeparator. +#include "styles/style_menu_icons.h" + +namespace { + +struct State final { + State(not_null session) : sender(&session->mtp()) { + } + MTP::Sender sender; +}; + +struct BankCardData final { + QString title; + std::vector links; +}; + +enum class Status { + Loading, + Resolved, + Failed, +}; + +void RequestResolveBankCard( + not_null state, + const QString &bankCard, + Fn done, + Fn fail) { + state->sender.request(MTPpayments_GetBankCardData( + MTP_string(bankCard) + )).done([=](const MTPpayments_BankCardData &result) { + auto bankCardData = BankCardData{ + .title = qs(result.data().vtitle()), + }; + for (const auto &tl : result.data().vopen_urls().v) { + const auto url = qs(tl.data().vurl()); + const auto name = qs(tl.data().vname()); + + bankCardData.links.emplace_back(EntityLinkData{ + .text = name, + .data = url, + }); + } + done(std::move(bankCardData)); + }).fail([=](const MTP::Error &error) { + fail(error.type()); + }).send(); +} + +class ResolveBankCardAction final : public Ui::Menu::ItemBase { +public: + ResolveBankCardAction( + not_null parent, + const style::Menu &st); + + void setStatus(Status status); + + bool isEnabled() const override; + not_null action() const override; + +protected: + int contentHeight() const override; + + void paintEvent(QPaintEvent *e) override; + +private: + void paint(Painter &p); + + const not_null _dummyAction; + const style::Menu &_st; + const int _height = 0; + Status _status = Status::Loading; + + Ui::Text::String _text; + +}; + +ResolveBankCardAction::ResolveBankCardAction( + not_null parent, + const style::Menu &st) +: ItemBase(parent, st) +, _dummyAction(Ui::CreateChild(parent)) +, _st(st) +, _height(st::groupCallJoinAsPhotoSize) { + setAcceptBoth(true); + initResizeHook(parent->sizeValue()); + setStatus(Status::Loading); +} + +void ResolveBankCardAction::setStatus(Status status) { + _status = status; + if (status == Status::Resolved) { + resize(width(), 0); + } else if (status == Status::Failed) { + _text.setText(_st.itemStyle, tr::lng_attach_failed(tr::now)); + } else if (status == Status::Loading) { + _text.setText(_st.itemStyle, tr::lng_contacts_loading(tr::now)); + } + update(); +} + +void ResolveBankCardAction::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto selected = false; + const auto height = contentHeight(); + if (selected && _st.itemBgOver->c.alpha() < 255) { + p.fillRect(0, 0, width(), height, _st.itemBg); + } + p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg); + + const auto &padding = st::groupCallJoinAsPadding; + const auto textLeft = padding.left() + + st::groupCallJoinAsPhotoSize + + padding.left(); + { + p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); + const auto w = width() - padding.left() - padding.right(); + _text.draw(p, Ui::Text::PaintContext{ + .position = QPoint( + (width() - w) / 2, + (height - _text.countHeight(w)) / 2), + .outerWidth = w, + .availableWidth = w, + .align = style::al_center, + .elisionLines = 2, + }); + } +} + +bool ResolveBankCardAction::isEnabled() const { + return false; +} + +not_null ResolveBankCardAction::action() const { + return _dummyAction; +} + +int ResolveBankCardAction::contentHeight() const { + if (_status == Status::Resolved) { + return 0; + } + return _height; +} + +} // namespace + +BankCardClickHandler::BankCardClickHandler( + not_null session, + QString text) +: _session(session) +, _text(text) { +} + +void BankCardClickHandler::onClick(ClickContext context) const { + if (context.button != Qt::LeftButton) { + return; + } + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + const auto pos = QCursor::pos(); + if (!controller) { + return; + } + const auto menu = Ui::CreateChild( + controller->content(), + st::popupMenuWithIcons); + + const auto bankCard = _text; + + const auto copy = [bankCard, show = controller->uiShow()] { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(bankCard)); + show->showToast(tr::lng_context_bank_card_copied(tr::now)); + }; + + menu->addAction( + tr::lng_context_bank_card_copy(tr::now), + copy, + &st::menuIconCopy); + + auto resolveBankCardAction = base::make_unique_q( + menu, + menu->st().menu); + const auto resolveBankCardRaw = resolveBankCardAction.get(); + + menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator); + + menu->addAction(std::move(resolveBankCardAction)); + + const auto addTitle = [=](const QString &name) { + auto button = base::make_unique_q( + menu, + menu->st().menu, + st::historyHasCustomEmoji, + st::historyBankCardMenuMultilinePosition, + TextWithEntities{ name }); + button->setClickedCallback(copy); + menu->addAction(std::move(button)); + }; + + const auto state = menu->lifetime().make_state( + &controller->session()); + RequestResolveBankCard( + state, + bankCard, + [=](BankCardData data) { + resolveBankCardRaw->setStatus(Status::Resolved); + for (auto &link : data.links) { + menu->addAction( + base::take(link.text), + [u = base::take(link.data)] { UrlClickHandler::Open(u); }, + &st::menuIconPayment); + } + if (!data.title.isEmpty()) { + addTitle(base::take(data.title)); + } + }, + [=](const QString &) { + resolveBankCardRaw->setStatus(Status::Failed); + }); + + menu->popup(pos); +} + +auto BankCardClickHandler::getTextEntity() const -> TextEntity { + return { EntityType::BankCard }; +} + +QString BankCardClickHandler::tooltip() const { + return _text; +} diff --git a/Telegram/SourceFiles/core/bank_card_click_handler.h b/Telegram/SourceFiles/core/bank_card_click_handler.h new file mode 100644 index 0000000000..ff45af82e1 --- /dev/null +++ b/Telegram/SourceFiles/core/bank_card_click_handler.h @@ -0,0 +1,30 @@ +/* +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 "ui/basic_click_handlers.h" + +namespace Main { +class Session; +} // namespace Main + +class BankCardClickHandler : public ClickHandler { +public: + BankCardClickHandler(not_null session, QString text); + + void onClick(ClickContext context) const override; + + TextEntity getTextEntity() const override; + + QString tooltip() const override; + +private: + const not_null _session; + QString _text; + +}; diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 83d287a164..3283ea2b35 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/local_url_handlers.h" #include "core/file_utilities.h" #include "core/application.h" +#include "core/bank_card_click_handler.h" #include "core/sandbox.h" #include "core/click_handler_types.h" #include "data/stickers/data_custom_emoji.h" @@ -256,6 +257,10 @@ std::shared_ptr UiIntegration::createLinkHandler( return my->session ? std::make_shared(my->session, data.text) : nullptr; + case EntityType::BankCard: + return my->session + ? std::make_shared(my->session, data.text) + : nullptr; } return Integration::createLinkHandler(data, context); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 201584c4c0..5563629ad2 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3464,6 +3464,7 @@ void HistoryItem::setText(const TextWithEntities &textWithEntities) { if (type == EntityType::Url || type == EntityType::CustomUrl || type == EntityType::Phone + || type == EntityType::BankCard || type == EntityType::Email) { _flags |= MessageFlag::HasTextLinks; break; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index f285966d14..0ae4d9ba54 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -949,6 +949,7 @@ historyHasCustomEmoji: FlatLabel(defaultFlatLabel) { minWidth: 80px; } historyHasCustomEmojiPosition: point(12px, 4px); +historyBankCardMenuMultilinePosition: point(18px, 4px); historyTranslateLabel: FlatLabel(defaultFlatLabel) { style: semiboldTextStyle; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 4c49be52ff..793772c0da 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -178,6 +178,7 @@ menuIconNftWear: icon {{ "menu/nft_wear", menuIconColor }}; menuIconNftTakeOff: icon {{ "menu/nft_takeoff", menuIconColor }}; menuIconShortcut: icon {{ "menu/shortcut", menuIconColor }}; menuIconHourglass: icon {{ "menu/hourglass", menuIconColor }}; +menuIconPayment: icon {{ "payments/payment_card", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px);