From a130bb1be6d3c3fbae888fd64f062b794afcd56e Mon Sep 17 00:00:00 2001 From: John Preston 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 4d8f378aed..ac3d5912b1 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 27ffd3b6c6..e44fd51a95 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 818ea361aa..2ec4a9cfe4 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 dab4ffd00b..3b495f1159 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 2eef9d698a..b5920e1310 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 session, rpl::producer> content); @@ -176,21 +177,21 @@ private: return query.trimmed().toLower(); } -LinksController::LinksController( +VenuesController::VenuesController( not_null session, rpl::producer> content) : _session(session) , _rows(std::move(content)) { } -void LinksController::prepare() { +void VenuesController::prepare() { _rows.value( ) | rpl::start_with_next([=](const std::vector &rows) { rebuild(rows); }, _lifetime); } -void LinksController::rebuild(const std::vector &rows) { +void VenuesController::rebuild(const std::vector &rows) { auto i = 0; auto count = delegate()->peerListFullRowsCount(); while (i < rows.size()) { @@ -209,24 +210,24 @@ void LinksController::rebuild(const std::vector &rows) { delegate()->peerListRefreshRows(); } -void LinksController::rowClicked(not_null row) { +void VenuesController::rowClicked(not_null row) { const auto venue = static_cast(row.get())->data(); venue; } -void LinksController::rowRightActionClicked(not_null row) { +void VenuesController::rowRightActionClicked(not_null 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(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>{ { "maps-places-in-area", tr::lng_maps_places_in_area }, @@ -358,6 +360,9 @@ void LinksController::rowPaintIcon( +
+
+
@@ -467,7 +472,7 @@ void SetupVenues( auto &lifetime = container->lifetime(); const auto delegate = lifetime.make_state( show); - const auto controller = lifetime.make_state( + const auto controller = lifetime.make_state( &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 e9c0d9801d..0adfc166fc 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 _geocoderAddress; rpl::variable _venueState;