From ba611d0f2dd71d76f40db1e143ea6feac5264b20 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Fri, 31 May 2024 18:58:14 +0300
Subject: [PATCH] Added initial api support of text phone entity in messages.

---
 Telegram/CMakeLists.txt                       |   2 +
 Telegram/Resources/langs/lang.strings         |   1 +
 .../SourceFiles/api/api_text_entities.cpp     |   9 +-
 .../SourceFiles/core/click_handler_types.h    |   2 +
 .../SourceFiles/core/phone_click_handler.cpp  | 325 ++++++++++++++++++
 .../SourceFiles/core/phone_click_handler.h    |  30 ++
 Telegram/SourceFiles/core/ui_integration.cpp  |   3 +
 Telegram/SourceFiles/history/history_item.cpp |   1 +
 Telegram/lib_ui                               |   2 +-
 9 files changed, 373 insertions(+), 2 deletions(-)
 create mode 100644 Telegram/SourceFiles/core/phone_click_handler.cpp
 create mode 100644 Telegram/SourceFiles/core/phone_click_handler.h

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<MTPMessageEntity> 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<Ui::RpWidget*> parent,
+		const style::Menu &st,
+		const QString &phone,
+		not_null<Window::SessionController*> controller);
+
+	bool isEnabled() const override;
+	not_null<QAction*> action() const override;
+
+	void handleKeyPress(not_null<QKeyEvent*> 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<QAction*> _dummyAction;
+	const style::Menu &_st;
+	rpl::variable<PeerData*> _peer;
+	rpl::variable<bool> _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<Ui::RpWidget*> parent,
+	const style::Menu &st,
+	const QString &phone,
+	not_null<Window::SessionController*> 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<QAction*> 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<QKeyEvent*> 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<Main::Session*> 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<ClickHandlerContext>();
+	const auto controller = my.sessionWindow.get();
+	const auto pos = QCursor::pos();
+	if (!controller) {
+		return;
+	}
+	const auto menu = Ui::CreateChild<Ui::PopupMenu>(
+		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<ResolvePhoneAction>(
+			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<Main::Session*> session, QString text);
+
+	void onClick(ClickContext context) const override;
+
+	TextEntity getTextEntity() const override;
+
+	QString tooltip() const override;
+
+private:
+	const not_null<Main::Session*> _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<ClickHandler> UiIntegration::createLinkHandler(
 		return std::make_shared<MonospaceClickHandler>(data.text, data.type);
 	case EntityType::Pre:
 		return std::make_shared<MonospaceClickHandler>(data.text, data.type);
+	case EntityType::Phone:
+		return std::make_shared<PhoneClickHandler>(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