Search for venues by location.

This commit is contained in:
John Preston 2024-07-11 18:15:17 +02:00
parent de52ac6b28
commit a130bb1be6
6 changed files with 213 additions and 36 deletions

View file

@ -1,7 +1,5 @@
:root { :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-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 { html {
@ -13,8 +11,6 @@ html {
body { body {
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 17px;
line-height: 25px;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;
@ -68,3 +64,57 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
#marker_shadow { #marker_shadow {
position: absolute; 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;
}
}

View file

@ -72,6 +72,7 @@ var LocationPicker = {
LocationPicker.map = new mapboxgl.Map(options); LocationPicker.map = new mapboxgl.Map(options);
LocationPicker.createMarker(center); LocationPicker.createMarker(center);
LocationPicker.trackMovement(); LocationPicker.trackMovement();
LocationPicker.initSearchVenueRipple();
}, },
marker: function() { marker: function() {
return document.getElementById('marker_drop'); return document.getElementById('marker_drop');
@ -93,13 +94,14 @@ var LocationPicker = {
LocationPicker.map.on('movestart', function() { LocationPicker.map.on('movestart', function() {
LocationPicker.marker().classList.add('moving'); LocationPicker.marker().classList.add('moving');
LocationPicker.clearMovingTimer(); LocationPicker.clearMovingTimer();
LocationPicker.notify({ event: 'movestart' }); LocationPicker.toggleSearchVenues(false);
LocationPicker.notify({ event: 'move_start' });
}); });
LocationPicker.map.on('moveend', function() { LocationPicker.map.on('moveend', function() {
LocationPicker.startMovingTimer(function() { LocationPicker.startMovingTimer(function() {
LocationPicker.marker().classList.remove('moving'); LocationPicker.marker().classList.remove('moving');
LocationPicker.notify({ LocationPicker.notify({
event: 'moveend', event: 'move_end',
latitude: LocationPicker.map.getCenter().lat, latitude: LocationPicker.map.getCenter().lat,
longitude: LocationPicker.map.getCenter().lng longitude: LocationPicker.map.getCenter().lng
}); });
@ -119,5 +121,79 @@ var LocationPicker = {
latitude: LocationPicker.map.getCenter().lat, latitude: LocationPicker.map.getCenter().lat,
longitude: LocationPicker.map.getCenter().lng 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);
},
}; };

View file

@ -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"place"_q, u"region"_q });
add({ u"country"_q }); add({ u"country"_q });
finishWith({ .name = names.join(", ") }); finishWith({ .name = names.join(", ") });
@ -215,4 +215,29 @@ void ResolveLocationAddress(
Platform::ResolveLocationAddress(location, language, std::move(done)); 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 } // namespace Core

View file

@ -33,12 +33,10 @@ struct GeoLocation {
explicit operator bool() const { explicit operator bool() const {
return !failed(); return !failed();
} }
friend inline bool operator==(
const GeoLocation&,
const GeoLocation&) = default;
}; };
[[nodiscard]] bool AreTheSame(const GeoLocation &a, const GeoLocation &b);
struct GeoAddress { struct GeoAddress {
QString name; QString name;

View file

@ -102,6 +102,7 @@ VenueRow::VenueRow(
void VenueRow::update(const VenueData &data) { void VenueRow::update(const VenueData &data) {
_data = data; _data = data;
setCustomStatus(data.address); setCustomStatus(data.address);
refreshName(st::pickLocationVenueItem);
} }
VenueData VenueRow::data() const { VenueData VenueRow::data() const {
@ -128,12 +129,12 @@ PaintRoundImageCallback VenueRow::generatePaintUserpicCallback(
}; };
} }
class LinksController final class VenuesController final
: public PeerListController : public PeerListController
, public VenueRowDelegate , public VenueRowDelegate
, public base::has_weak_ptr { , public base::has_weak_ptr {
public: public:
LinksController( VenuesController(
not_null<Main::Session*> session, not_null<Main::Session*> session,
rpl::producer<std::vector<VenueData>> content); rpl::producer<std::vector<VenueData>> content);
@ -176,21 +177,21 @@ private:
return query.trimmed().toLower(); return query.trimmed().toLower();
} }
LinksController::LinksController( VenuesController::VenuesController(
not_null<Main::Session*> session, not_null<Main::Session*> session,
rpl::producer<std::vector<VenueData>> content) rpl::producer<std::vector<VenueData>> content)
: _session(session) : _session(session)
, _rows(std::move(content)) { , _rows(std::move(content)) {
} }
void LinksController::prepare() { void VenuesController::prepare() {
_rows.value( _rows.value(
) | rpl::start_with_next([=](const std::vector<VenueData> &rows) { ) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
rebuild(rows); rebuild(rows);
}, _lifetime); }, _lifetime);
} }
void LinksController::rebuild(const std::vector<VenueData> &rows) { void VenuesController::rebuild(const std::vector<VenueData> &rows) {
auto i = 0; auto i = 0;
auto count = delegate()->peerListFullRowsCount(); auto count = delegate()->peerListFullRowsCount();
while (i < rows.size()) { while (i < rows.size()) {
@ -209,24 +210,24 @@ void LinksController::rebuild(const std::vector<VenueData> &rows) {
delegate()->peerListRefreshRows(); 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(); const auto venue = static_cast<VenueRow*>(row.get())->data();
venue; venue;
} }
void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) { void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true); delegate()->peerListShowRowMenu(row, true);
} }
Main::Session &LinksController::session() const { Main::Session &VenuesController::session() const {
return *_session; return *_session;
} }
void LinksController::appendRow(const VenueData &data) { void VenuesController::appendRow(const VenueData &data) {
delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data)); delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));
} }
void LinksController::rowPaintIcon( void VenuesController::rowPaintIcon(
QPainter &p, QPainter &p,
int x, int x,
int y, int y,
@ -331,6 +332,7 @@ void LinksController::rowPaintIcon(
{ "window-bg-over", &st::windowBgOver }, { "window-bg-over", &st::windowBgOver },
{ "window-bg-ripple", &st::windowBgRipple }, { "window-bg-ripple", &st::windowBgRipple },
{ "window-active-text-fg", &st::windowActiveTextFg }, { "window-active-text-fg", &st::windowActiveTextFg },
{ "history-to-down-shadow", &st::historyToDownShadow },
}; };
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{ static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
{ "maps-places-in-area", tr::lng_maps_places_in_area }, { "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' /> <link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
</head> </head>
<body> <body>
<div id="search_venues">
<div id="search_venues_inner"><span id="search_venues_content"></span></div>
</div>
<div id="marker"> <div id="marker">
<div id="marker_shadow" style="transform: translate(0px, -14px);"> <div id="marker_shadow" style="transform: translate(0px, -14px);">
<svg display="block" height="41px" width="27px" viewBox="0 0 27 41"> <svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
@ -467,7 +472,7 @@ void SetupVenues(
auto &lifetime = container->lifetime(); auto &lifetime = container->lifetime();
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>( const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
show); show);
const auto controller = lifetime.make_state<LinksController>( const auto controller = lifetime.make_state<VenuesController>(
&show->session(), &show->session(),
std::move(value)); std::move(value));
controller->setStyleOverrides(&st::pickLocationVenueList); controller->setStyleOverrides(&st::pickLocationVenueList);
@ -687,17 +692,40 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
const auto lon = object.value("longitude").toDouble(); const auto lon = object.value("longitude").toDouble();
_callback({ lat, lon }); _callback({ lat, lon });
close(); close();
} else if (event == u"movestart"_q) { } else if (event == u"move_start"_q) {
_geocoderAddress = QString(); if (const auto now = _geocoderAddress.current()
; !now.isEmpty()) {
_geocoderSavedAddress = now;
_geocoderAddress = QString();
}
base::take(_geocoderResolvePostponed);
_geocoderResolveTimer.cancel(); _geocoderResolveTimer.cancel();
} else if (event == u"moveend"_q) { } else if (event == u"move_end"_q) {
const auto lat = object.value("latitude").toDouble(); const auto lat = object.value("latitude").toDouble();
const auto lon = object.value("longitude").toDouble(); const auto lon = object.value("longitude").toDouble();
_geocoderResolvePostponed = Core::GeoLocation{ const auto location = Core::GeoLocation{
.point = { lat, lon }, .point = { lat, lon },
.accuracy = Core::GeoLocationAccuracy::Exact, .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) { void LocationPicker::resolveAddress(Core::GeoLocation location) {
if (_geocoderResolvingFor == location) { if (AreTheSame(_geocoderResolvingFor, location)) {
return; return;
} }
_geocoderResolvingFor = location; _geocoderResolvingFor = location;
const auto done = [=](Core::GeoAddress address) { const auto done = [=](Core::GeoAddress address) {
if (_geocoderResolvingFor != location) { if (!AreTheSame(_geocoderResolvingFor, location)) {
return; return;
} else if (address) { } else if (address) {
_geocoderAddress = address.name; _geocoderAddress = address.name;
@ -809,14 +837,13 @@ void LocationPicker::venuesRequest(
QString query) { QString query) {
query = NormalizeVenuesQuery(query); query = NormalizeVenuesQuery(query);
auto &cache = _venuesCache[query]; auto &cache = _venuesCache[query];
const auto i = ranges::find( const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {
cache, return AreTheSame(v.location, location);
location, });
&VenuesCacheEntry::location);
if (i != end(cache)) { if (i != end(cache)) {
_venueState = i->result; _venueState = i->result;
return; return;
} else if (_venuesRequestLocation == location } else if (AreTheSame(_venuesRequestLocation, location)
&& _venuesRequestQuery == query) { && _venuesRequestQuery == query) {
return; return;
} else if (const auto oldRequestId = base::take(_venuesRequestId)) { } else if (const auto oldRequestId = base::take(_venuesRequestId)) {
@ -888,7 +915,7 @@ void LocationPicker::resolveCurrentLocation() {
using namespace Core; using namespace Core;
const auto window = _window.get(); const auto window = _window.get();
ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) { ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
const auto changed = (LastExactLocation != location); const auto changed = !AreTheSame(LastExactLocation, location);
if (location.accuracy != GeoLocationAccuracy::Exact || !changed) { if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
return; return;
} }

View file

@ -129,6 +129,7 @@ private:
base::Timer _geocoderResolveTimer; base::Timer _geocoderResolveTimer;
Core::GeoLocation _geocoderResolvePostponed; Core::GeoLocation _geocoderResolvePostponed;
Core::GeoLocation _geocoderResolvingFor; Core::GeoLocation _geocoderResolvingFor;
QString _geocoderSavedAddress;
rpl::variable<QString> _geocoderAddress; rpl::variable<QString> _geocoderAddress;
rpl::variable<PickerVenueState> _venueState; rpl::variable<PickerVenueState> _venueState;