From a130bb1be6d3c3fbae888fd64f062b794afcd56e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 11 Jul 2024 18:15:17 +0200
Subject: [PATCH] Search for venues by location.

---
 Telegram/Resources/picker_html/picker.css     | 58 ++++++++++++-
 Telegram/Resources/picker_html/picker.js      | 82 ++++++++++++++++++-
 .../SourceFiles/core/current_geo_location.cpp | 27 +++++-
 .../SourceFiles/core/current_geo_location.h   |  6 +-
 .../ui/controls/location_picker.cpp           | 75 +++++++++++------
 .../SourceFiles/ui/controls/location_picker.h |  1 +
 6 files changed, 213 insertions(+), 36 deletions(-)

diff --git a/Telegram/Resources/picker_html/picker.css b/Telegram/Resources/picker_html/picker.css
index 4d8f378ae..ac3d5912b 100644
--- a/Telegram/Resources/picker_html/picker.css
+++ b/Telegram/Resources/picker_html/picker.css
@@ -1,7 +1,5 @@
 :root {
 	--font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif;
-	--font-serif: Iowan Old Style, Apple Garamond, Baskerville, Georgia, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
-	--font-mono: Menlo, Cascadia Code, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
 }
 
 html {
@@ -13,8 +11,6 @@ html {
 
 body {
 	font-family: var(--font-sans);
-	font-size: 17px;
-	line-height: 25px;
 	width: 100%;
 	height: 100%;
 	padding: 0;
@@ -68,3 +64,57 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
 #marker_shadow {
 	position: absolute;
 }
+#search_venues {
+	position: absolute;
+	left: 50%;
+	transform: translateX(-50%);
+	z-index: 2;
+	top: -30px;
+	transition: top 200ms ease-in-out;
+}
+#search_venues.shown {
+	top: 6px;
+}
+#search_venues_inner {
+	position: relative;
+	overflow: hidden;
+	font-size: 13px;
+	font-weight: 500;
+	background: var(--td-window-bg);
+	color: var(--td-window-active-text-fg);
+	cursor: pointer;
+	border-radius: 14px;
+	padding: 5px 12px 6px;
+	box-shadow: 0 0 3px 0px var(--td-history-to-down-shadow);
+}
+#search_venues_inner:hover {
+	background: var(--td-window-bg-over);
+}
+#search_venues_content {
+	position: relative;
+	z-index: 2;
+}
+#search_venues_content:before {
+	content: var(--td-lng-maps-places-in-area);
+}
+#search_venues_inner .ripple .inner {
+	position: absolute;
+	border-radius: 50%;
+	transform: scale(0);
+	opacity: 1;
+	animation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
+	background-color: var(--td-window-bg-ripple);
+}
+#search_venues_inner .ripple.hiding {
+	animation: fadeOut 200ms linear forwards;
+}
+@keyframes ripple {
+	to {
+		transform: scale(2);
+	}
+}
+@keyframes fadeOut {
+	to {
+		opacity: 0;
+	}
+}
diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js
index 27ffd3b6c..e44fd51a9 100644
--- a/Telegram/Resources/picker_html/picker.js
+++ b/Telegram/Resources/picker_html/picker.js
@@ -72,6 +72,7 @@ var LocationPicker = {
 		LocationPicker.map = new mapboxgl.Map(options);
 		LocationPicker.createMarker(center);
 		LocationPicker.trackMovement();
+		LocationPicker.initSearchVenueRipple();
 	},
 	marker: function() {
 		return document.getElementById('marker_drop');
@@ -93,13 +94,14 @@ var LocationPicker = {
 		LocationPicker.map.on('movestart', function() {
 			LocationPicker.marker().classList.add('moving');
 			LocationPicker.clearMovingTimer();
-			LocationPicker.notify({ event: 'movestart' });
+			LocationPicker.toggleSearchVenues(false);
+			LocationPicker.notify({ event: 'move_start' });
 		});
 		LocationPicker.map.on('moveend', function() {
 			LocationPicker.startMovingTimer(function() {
 				LocationPicker.marker().classList.remove('moving');
 				LocationPicker.notify({
-					event: 'moveend',
+					event: 'move_end',
 					latitude: LocationPicker.map.getCenter().lat,
 					longitude: LocationPicker.map.getCenter().lng
 				});
@@ -119,5 +121,79 @@ var LocationPicker = {
 			latitude: LocationPicker.map.getCenter().lat,
 			longitude: LocationPicker.map.getCenter().lng
 		});
-	}
+	},
+	addRipple: function (button, x, y) {
+		const ripple = document.createElement('span');
+		ripple.classList.add('ripple');
+
+		const inner = document.createElement('span');
+		inner.classList.add('inner');
+
+		var rect = button.getBoundingClientRect();
+		x -= rect.x;
+		y -= rect.y;
+
+		const mx = button.clientWidth - x;
+		const my = button.clientHeight - y;
+		const sq1 = x * x + y * y;
+		const sq2 = mx * mx + y * y;
+		const sq3 = x * x + my * my;
+		const sq4 = mx * mx + my * my;
+		const radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4));
+
+		inner.style.width = inner.style.height = `${2 * radius}px`;
+		inner.style.left = `${x - radius}px`;
+		inner.style.top = `${y - radius}px`;
+		inner.classList.add('inner');
+
+		ripple.addEventListener('animationend', function (e) {
+			if (e.animationName === 'fadeOut') {
+				ripple.remove();
+			}
+		});
+
+		ripple.appendChild(inner);
+		button.appendChild(ripple);
+	},
+	stopRipples: function (button) {
+		const id = button.id ? button.id : button;
+		button = document.getElementById(id);
+		const ripples = button.getElementsByClassName('ripple');
+		for (var i = 0; i < ripples.length; ++i) {
+			const ripple = ripples[i];
+			if (!ripple.classList.contains('hiding')) {
+				ripple.classList.add('hiding');
+			}
+		}
+	},
+	initSearchVenueRipple: function() {
+		var button = document.getElementById('search_venues_inner');
+		button.addEventListener('mousedown', function (e) {
+			LocationPicker.addRipple(e.currentTarget, e.clientX, e.clientY);
+			LocationPicker.searchVenuesPressed = true;
+		});
+		button.addEventListener('mouseup', function (e) {
+			const id = e.currentTarget.id;
+			setTimeout(function () {
+				LocationPicker.stopRipples(id);
+			}, 0);
+			if (LocationPicker.searchVenuesPressed) {
+				LocationPicker.searchVenuesPressed = false;
+				LocationPicker.toggleSearchVenues(false);
+				LocationPicker.notify({
+					event: 'search_venues',
+					latitude: LocationPicker.map.getCenter().lat,
+					longitude: LocationPicker.map.getCenter().lng
+				});
+			}
+		});
+		button.addEventListener('mouseleave', function (e) {
+			LocationPicker.stopRipples(e.currentTarget);
+			LocationPicker.searchVenuesPressed = false;
+		});
+	},
+	toggleSearchVenues: function(shown) {
+		var button = document.getElementById('search_venues');
+		button.classList.toggle('shown', shown);
+	},
 };
diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp
index 818ea361a..2ec4a9cfe 100644
--- a/Telegram/SourceFiles/core/current_geo_location.cpp
+++ b/Telegram/SourceFiles/core/current_geo_location.cpp
@@ -148,7 +148,7 @@ void ResolveLocationAddressGeneric(
 				}
 			}
 		};
-		add({ u"address"_q, u"street"_q, u"neighborhood"_q });
+		add({ /*u"address"_q, u"street"_q, */u"neighborhood"_q });
 		add({ u"place"_q, u"region"_q });
 		add({ u"country"_q });
 		finishWith({ .name = names.join(", ") });
@@ -215,4 +215,29 @@ void ResolveLocationAddress(
 	Platform::ResolveLocationAddress(location, language, std::move(done));
 }
 
+bool AreTheSame(const GeoLocation &a, const GeoLocation &b) {
+	if (a.accuracy != GeoLocationAccuracy::Exact
+		|| b.accuracy != GeoLocationAccuracy::Exact) {
+		return false;
+	}
+	const auto normalize = [](float64 value) {
+		value = std::fmod(value + 180., 360.);
+		return (value + (value < 0. ? 360. : 0.)) - 180.;
+	};
+	constexpr auto kEpsilon = 0.0001;
+	const auto lon1 = normalize(a.point.y());
+	const auto lon2 = normalize(b.point.y());
+	const auto diffLat = std::abs(a.point.x() - b.point.x());
+	if (std::abs(a.point.x()) >= (90. - kEpsilon)
+		|| std::abs(b.point.x()) >= (90. - kEpsilon)) {
+		return diffLat <= kEpsilon;
+	}
+	auto diffLon = std::abs(lon1 - lon2);
+	if (diffLon > 180.) {
+		diffLon = 360. - diffLon;
+	}
+
+	return diffLat <= kEpsilon && diffLon <= kEpsilon;
+}
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h
index dab4ffd00..3b495f115 100644
--- a/Telegram/SourceFiles/core/current_geo_location.h
+++ b/Telegram/SourceFiles/core/current_geo_location.h
@@ -33,12 +33,10 @@ struct GeoLocation {
 	explicit operator bool() const {
 		return !failed();
 	}
-
-	friend inline bool operator==(
-		const GeoLocation&,
-		const GeoLocation&) = default;
 };
 
+[[nodiscard]] bool AreTheSame(const GeoLocation &a, const GeoLocation &b);
+
 struct GeoAddress {
 	QString name;
 
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 2eef9d698..b5920e131 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -102,6 +102,7 @@ VenueRow::VenueRow(
 void VenueRow::update(const VenueData &data) {
 	_data = data;
 	setCustomStatus(data.address);
+	refreshName(st::pickLocationVenueItem);
 }
 
 VenueData VenueRow::data() const {
@@ -128,12 +129,12 @@ PaintRoundImageCallback VenueRow::generatePaintUserpicCallback(
 	};
 }
 
-class LinksController final
+class VenuesController final
 	: public PeerListController
 	, public VenueRowDelegate
 	, public base::has_weak_ptr {
 public:
-	LinksController(
+	VenuesController(
 		not_null<Main::Session*> session,
 		rpl::producer<std::vector<VenueData>> content);
 
@@ -176,21 +177,21 @@ private:
 	return query.trimmed().toLower();
 }
 
-LinksController::LinksController(
+VenuesController::VenuesController(
 	not_null<Main::Session*> session,
 	rpl::producer<std::vector<VenueData>> content)
 : _session(session)
 , _rows(std::move(content)) {
 }
 
-void LinksController::prepare() {
+void VenuesController::prepare() {
 	_rows.value(
 	) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
 		rebuild(rows);
 	}, _lifetime);
 }
 
-void LinksController::rebuild(const std::vector<VenueData> &rows) {
+void VenuesController::rebuild(const std::vector<VenueData> &rows) {
 	auto i = 0;
 	auto count = delegate()->peerListFullRowsCount();
 	while (i < rows.size()) {
@@ -209,24 +210,24 @@ void LinksController::rebuild(const std::vector<VenueData> &rows) {
 	delegate()->peerListRefreshRows();
 }
 
-void LinksController::rowClicked(not_null<PeerListRow*> row) {
+void VenuesController::rowClicked(not_null<PeerListRow*> row) {
 	const auto venue = static_cast<VenueRow*>(row.get())->data();
 	venue;
 }
 
-void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
+void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
 	delegate()->peerListShowRowMenu(row, true);
 }
 
-Main::Session &LinksController::session() const {
+Main::Session &VenuesController::session() const {
 	return *_session;
 }
 
-void LinksController::appendRow(const VenueData &data) {
+void VenuesController::appendRow(const VenueData &data) {
 	delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));
 }
 
-void LinksController::rowPaintIcon(
+void VenuesController::rowPaintIcon(
 		QPainter &p,
 		int x,
 		int y,
@@ -331,6 +332,7 @@ void LinksController::rowPaintIcon(
 		{ "window-bg-over", &st::windowBgOver },
 		{ "window-bg-ripple", &st::windowBgRipple },
 		{ "window-active-text-fg", &st::windowActiveTextFg },
+		{ "history-to-down-shadow", &st::historyToDownShadow },
 	};
 	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
 		{ "maps-places-in-area", tr::lng_maps_places_in_area },
@@ -358,6 +360,9 @@ void LinksController::rowPaintIcon(
 		<link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
 	</head>
 	<body>
+		<div id="search_venues">
+			<div id="search_venues_inner"><span id="search_venues_content"></span></div>
+		</div>
 		<div id="marker">
 			<div id="marker_shadow" style="transform: translate(0px, -14px);">
 <svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
@@ -467,7 +472,7 @@ void SetupVenues(
 	auto &lifetime = container->lifetime();
 	const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
 		show);
-	const auto controller = lifetime.make_state<LinksController>(
+	const auto controller = lifetime.make_state<VenuesController>(
 		&show->session(),
 		std::move(value));
 	controller->setStyleOverrides(&st::pickLocationVenueList);
@@ -687,17 +692,40 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 				const auto lon = object.value("longitude").toDouble();
 				_callback({ lat, lon });
 				close();
-			} else if (event == u"movestart"_q) {
-				_geocoderAddress = QString();
+			} else if (event == u"move_start"_q) {
+				if (const auto now = _geocoderAddress.current()
+					; !now.isEmpty()) {
+					_geocoderSavedAddress = now;
+					_geocoderAddress = QString();
+				}
+				base::take(_geocoderResolvePostponed);
 				_geocoderResolveTimer.cancel();
-			} else if (event == u"moveend"_q) {
+			} else if (event == u"move_end"_q) {
 				const auto lat = object.value("latitude").toDouble();
 				const auto lon = object.value("longitude").toDouble();
-				_geocoderResolvePostponed = Core::GeoLocation{
+				const auto location = Core::GeoLocation{
 					.point = { lat, lon },
 					.accuracy = Core::GeoLocationAccuracy::Exact,
 				};
-				_geocoderResolveTimer.callOnce(kResolveAddressDelay);
+				if (AreTheSame(_geocoderResolvingFor, location)
+					&& !_geocoderSavedAddress.isEmpty()) {
+					_geocoderAddress = base::take(_geocoderSavedAddress);
+					_geocoderResolveTimer.cancel();
+				} else {
+					_geocoderResolvePostponed = location;
+					_geocoderResolveTimer.callOnce(kResolveAddressDelay);
+				}
+				if (!AreTheSame(_venuesRequestLocation, location)) {
+					_webview->eval(
+						"LocationPicker.toggleSearchVenues(true);");
+				}
+			} else if (event == u"search_venues"_q) {
+				const auto lat = object.value("latitude").toDouble();
+				const auto lon = object.value("longitude").toDouble();
+				venuesRequest({
+					.point = { lat, lon },
+					.accuracy = Core::GeoLocationAccuracy::Exact,
+				});
 			}
 		});
 	});
@@ -759,12 +787,12 @@ void LocationPicker::resolveAddressByTimer() {
 }
 
 void LocationPicker::resolveAddress(Core::GeoLocation location) {
-	if (_geocoderResolvingFor == location) {
+	if (AreTheSame(_geocoderResolvingFor, location)) {
 		return;
 	}
 	_geocoderResolvingFor = location;
 	const auto done = [=](Core::GeoAddress address) {
-		if (_geocoderResolvingFor != location) {
+		if (!AreTheSame(_geocoderResolvingFor, location)) {
 			return;
 		} else if (address) {
 			_geocoderAddress = address.name;
@@ -809,14 +837,13 @@ void LocationPicker::venuesRequest(
 		QString query) {
 	query = NormalizeVenuesQuery(query);
 	auto &cache = _venuesCache[query];
-	const auto i = ranges::find(
-		cache,
-		location,
-		&VenuesCacheEntry::location);
+	const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {
+		return AreTheSame(v.location, location);
+	});
 	if (i != end(cache)) {
 		_venueState = i->result;
 		return;
-	} else if (_venuesRequestLocation == location
+	} else if (AreTheSame(_venuesRequestLocation, location)
 		&& _venuesRequestQuery == query) {
 		return;
 	} else if (const auto oldRequestId = base::take(_venuesRequestId)) {
@@ -888,7 +915,7 @@ void LocationPicker::resolveCurrentLocation() {
 	using namespace Core;
 	const auto window = _window.get();
 	ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
-		const auto changed = (LastExactLocation != location);
+		const auto changed = !AreTheSame(LastExactLocation, location);
 		if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
 			return;
 		}
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index e9c0d9801..0adfc166f 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -129,6 +129,7 @@ private:
 	base::Timer _geocoderResolveTimer;
 	Core::GeoLocation _geocoderResolvePostponed;
 	Core::GeoLocation _geocoderResolvingFor;
+	QString _geocoderSavedAddress;
 	rpl::variable<QString> _geocoderAddress;
 
 	rpl::variable<PickerVenueState> _venueState;