diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8b081b263..1517a952e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -445,6 +445,8 @@ PRIVATE core/launcher.h core/local_url_handlers.cpp core/local_url_handlers.h + core/phone_click_handler.cpp + core/phone_click_handler.h core/sandbox.cpp core/sandbox.h core/shortcuts.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1b8491c85..bf8781ace 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3427,6 +3427,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_add_contact" = "Create"; "lng_add_contact_button" = "New contact"; "lng_contacts_header" = "Contacts"; +"lng_menu_not_contact" = "This number is not on Telegram"; "lng_contacts_hidden_stories" = "Hidden Stories"; "lng_contacts_stories_status#one" = "{count} story"; "lng_contacts_stories_status#other" = "{count} stories"; diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index 93d5cc5f3..067cc6c0c 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -178,7 +178,11 @@ EntitiesInText EntitiesFromMTP( }); } }, [&](const MTPDmessageEntityPhone &d) { - // Skipping phones. + result.push_back({ + EntityType::Phone, + d.voffset().v, + d.vlength().v, + }); }, [&](const MTPDmessageEntityCashtag &d) { result.push_back({ EntityType::Cashtag, @@ -266,6 +270,9 @@ MTPVector EntitiesToMTP( case EntityType::Email: { v.push_back(MTP_messageEntityEmail(offset, length)); } break; + case EntityType::Phone: { + v.push_back(MTP_messageEntityPhone(offset, length)); + } break; case EntityType::Hashtag: { v.push_back(MTP_messageEntityHashtag(offset, length)); } break; diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index e064d1e2d..20879a0ab 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -52,6 +52,8 @@ struct ClickHandlerContext { }; Q_DECLARE_METATYPE(ClickHandlerContext); +class PhoneClickHandler; + class HiddenUrlClickHandler : public UrlClickHandler { public: HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) { diff --git a/Telegram/SourceFiles/core/phone_click_handler.cpp b/Telegram/SourceFiles/core/phone_click_handler.cpp new file mode 100644 index 000000000..e510cfa04 --- /dev/null +++ b/Telegram/SourceFiles/core/phone_click_handler.cpp @@ -0,0 +1,325 @@ +/* +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/phone_click_handler.h" + +#include "core/click_handler_types.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "mtproto/sender.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/menu/menu_item_base.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 { + +[[nodiscard]] QString Trim(QString text) { + return text + .replace('+', QString()) + .replace(' ', QString()) + .replace('-', QString()); +} + +class ResolvePhoneAction final : public Ui::Menu::ItemBase { +public: + ResolvePhoneAction( + not_null parent, + const style::Menu &st, + const QString &phone, + not_null controller); + + bool isEnabled() const override; + not_null action() const override; + + void handleKeyPress(not_null e) override; + +protected: + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + + int contentHeight() const override; + +private: + void prepare(); + void paint(Painter &p); + + const not_null _dummyAction; + const style::Menu &_st; + rpl::variable _peer; + rpl::variable _loaded; + Ui::PeerUserpicView _userpicView; + + MTP::Sender _api; + + Ui::Text::String _above; + Ui::Text::String _below; + int _aboveWidth = 0; + int _belowWidth = 0; + const int _height = 0; + +}; + +ResolvePhoneAction::ResolvePhoneAction( + not_null parent, + const style::Menu &st, + const QString &phone, + not_null controller) +: ItemBase(parent, st) +, _dummyAction(new QAction(parent)) +, _st(st) +, _api(&controller->session().mtp()) +, _height(rect::m::sum::v(st::groupCallJoinAsPadding) + + st::groupCallJoinAsPhotoSize) { + setAcceptBoth(true); + initResizeHook(parent->sizeValue()); + setClickedCallback([=] { + if (const auto peer = _peer.current()) { + controller->showPeerInfo(peer); + } + }); + + const auto formattedPhone = Trim(phone); + + const auto owner = &controller->session().data(); + + if (const auto peer = owner->userByPhone(formattedPhone)) { + _peer = peer; + _loaded.force_assign(true); + } else { + _api.request(MTPcontacts_ResolvePhone( + MTP_string(phone) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + result.match([&](const MTPDcontacts_resolvedPeer &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + if (const auto peerId = peerFromMTP(data.vpeer())) { + _peer = owner->peer(peerId); + } + _loaded.force_assign(true); + }); + }).fail([=](const MTP::Error &error) { + if (error.code() == 400) { + _peer.force_assign(nullptr); + _loaded.force_assign(true); + } + }).send(); + } + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + paint(p); + }, lifetime()); + + enableMouseSelecting(); + prepare(); +} + +void ResolvePhoneAction::paint(Painter &p) { + const auto selected = isSelected() && _peer.current(); + 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); + if (isEnabled()) { + paintRipple(p, 0, 0); + } + + const auto &padding = st::groupCallJoinAsPadding; + const auto textLeft = padding.left() + + st::groupCallJoinAsPhotoSize + + padding.left(); + if (const auto peer = _peer.current()) { + peer->paintUserpic( + p, + _userpicView, + padding.left(), + padding.top(), + st::groupCallJoinAsPhotoSize); + p.setPen(selected ? _st.itemFgOver : _st.itemFg); + _above.drawLeftElided( + p, + textLeft, + st::groupCallJoinAsTextTop, + width() - textLeft - padding.right(), + width()); + p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); + _below.drawLeftElided( + p, + textLeft, + st::groupCallJoinAsNameTop, + _belowWidth, + width()); + } else { + p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); + p.drawText(rect() - padding, _below.toString(), style::al_center); + } +} + +void ResolvePhoneAction::prepare() { + rpl::combine( + tr::lng_context_view_profile(), + _peer.value( + ) | rpl::map([](PeerData *peer) { + return peer + ? Info::Profile::NameValue(peer) + : rpl::single(QString()); + }) | rpl::flatten_latest(), + tr::lng_menu_not_contact(), + _loaded.value( + ) | rpl::map([](bool loaded) { + return loaded + ? rpl::single(QString()) + : tr::lng_contacts_loading(); + }) | rpl::flatten_latest() + ) | rpl::start_with_next([=]( + QString text, + QString name, + QString no, + QString loading) { + const auto &padding = st::groupCallJoinAsPadding; + QWidget::setAttribute( + Qt::WA_TransparentForMouseEvents, + !_peer.current()); + const auto above = name; + const auto below = !loading.isEmpty() + ? loading + : name.isEmpty() + ? no + : text; + const auto options = kDefaultTextOptions; + const auto tempWidth = [&] { + _below.setMarkedText(_st.itemStyle, { text }, options); + return _below.maxWidth(); + }(); + _above.setMarkedText(_st.itemStyle, { above }, options); + _below.setMarkedText(_st.itemStyle, { below }, options); + const auto textWidth = _above.maxWidth(); + const auto nameWidth = _below.maxWidth(); + const auto textLeft = padding.left() + + st::groupCallJoinAsPhotoSize + + padding.left(); + const auto w = std::clamp( + (textLeft + tempWidth + padding.right()), + _st.widthMin, + _st.widthMax); + setMinWidth(w); + _aboveWidth = w - textLeft - padding.right(); + _belowWidth = w + - ((loading.isEmpty() && name.isEmpty()) ? 0 : textLeft) + - padding.right(); + update(); + }, lifetime()); +} + +bool ResolvePhoneAction::isEnabled() const { + return true; +} + +not_null ResolvePhoneAction::action() const { + return _dummyAction; +} + +QPoint ResolvePhoneAction::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()); +} + +QImage ResolvePhoneAction::prepareRippleMask() const { + return Ui::RippleAnimation::RectMask(size()); +} + +int ResolvePhoneAction::contentHeight() const { + return _height; +} + +void ResolvePhoneAction::handleKeyPress(not_null e) { + if (!isSelected() || !_peer.current()) { + return; + } + const auto key = e->key(); + if (key == Qt::Key_Enter || key == Qt::Key_Return) { + setClicked(Ui::Menu::TriggeredSource::Keyboard); + } +} + +} // namespace + +PhoneClickHandler::PhoneClickHandler( + not_null session, + QString text) +: _session(session) +, _text(text) { +} + +void PhoneClickHandler::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 phone = _text; + +#if 0 + const auto maybeContact = [&]() -> PeerData* { + const auto &chats = controller->session().data().contactsList(); + for (const auto &row : chats->all()) { + if (const auto history = row->history()) { + if (const auto user = history->peer->asUser()) { + if (Trim(user->phone()) == Trim(phone)) { + return user; + } + } + } + } + return nullptr; + }(); +#endif + + menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(phone.trimmed())); + }, &st::menuIconCopy); + + menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator); + + menu->addAction( + base::make_unique_q( + menu, + menu->st().menu, + phone, + controller)); + + menu->popup(pos); +} + +auto PhoneClickHandler::getTextEntity() const -> TextEntity { + return { EntityType::Phone }; +} + +QString PhoneClickHandler::tooltip() const { + return _text; +} diff --git a/Telegram/SourceFiles/core/phone_click_handler.h b/Telegram/SourceFiles/core/phone_click_handler.h new file mode 100644 index 000000000..bed2be2c5 --- /dev/null +++ b/Telegram/SourceFiles/core/phone_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 PhoneClickHandler : public ClickHandler { +public: + PhoneClickHandler(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 c1ef63e1b..f3de162ce 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "platform/platform_specific.h" #include "boxes/url_auth_box.h" +#include "core/phone_click_handler.h" #include "main/main_account.h" #include "main/main_session.h" #include "main/main_app_config.h" @@ -217,6 +218,8 @@ std::shared_ptr UiIntegration::createLinkHandler( return std::make_shared(data.text, data.type); case EntityType::Pre: return std::make_shared(data.text, data.type); + case EntityType::Phone: + return std::make_shared(my->session, data.text); } return Integration::createLinkHandler(data, context); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 88983c580..ba8b4fb39 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3127,6 +3127,7 @@ void HistoryItem::setText(const TextWithEntities &textWithEntities) { auto type = entity.type(); if (type == EntityType::Url || type == EntityType::CustomUrl + || type == EntityType::Phone || type == EntityType::Email) { _flags |= MessageFlag::HasTextLinks; break; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index d0514b2b0..444003724 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit d0514b2b022043b3777b06d6068232aa4cda7e80 +Subproject commit 4440037244bd0175752b82ee1177c676a5340f5c