/* 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 "ui/controls/location_picker.h" #include "apiwrap.h" #include "base/platform/base_platform_info.h" #include "boxes/peer_list_box.h" #include "core/current_geo_location.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" #include "data/data_location.h" #include "data/data_session.h" #include "data/data_user.h" #include "dialogs/ui/chat_search_empty.h" // Dialogs::SearchEmpty. #include "lang/lang_instance.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "main/session/session_show.h" #include "main/main_session.h" #include "mtproto/mtproto_config.h" #include "ui/effects/radial_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/separate_panel.h" #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/webview_helpers.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_embed.h" #include "webview/webview_interface.h" #include "window/themes/window_theme.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_window.h" #include "styles/style_settings.h" // settingsCloudPasswordIconSize #include "styles/style_layers.h" // boxDividerHeight #include #include #include #include #include #include namespace Ui { namespace { constexpr auto kResolveAddressDelay = 3 * crl::time(1000); constexpr auto kSearchDebounceDelay = crl::time(900); #ifdef Q_OS_MAC const auto kProtocolOverride = "mapboxapihelper"; #else // Q_OS_MAC const auto kProtocolOverride = ""; #endif // Q_OS_MAC Core::GeoLocation LastExactLocation; using VenueData = Data::InputVenue; class VenueRowDelegate { public: virtual void rowPaintIcon( QPainter &p, int x, int y, int size, const QString &type) = 0; }; class VenueRow final : public PeerListRow { public: VenueRow(not_null delegate, const VenueData &data); void update(const VenueData &data); [[nodiscard]] VenueData data() const; QString generateName() override; QString generateShortName() override; PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; private: const not_null _delegate; VenueData _data; }; VenueRow::VenueRow( not_null delegate, const VenueData &data) : PeerListRow(UniqueRowIdFromString(data.id)) , _delegate(delegate) , _data(data) { setCustomStatus(data.address); } void VenueRow::update(const VenueData &data) { _data = data; setCustomStatus(data.address); refreshName(st::pickLocationVenueItem); } VenueData VenueRow::data() const { return _data; } QString VenueRow::generateName() { return _data.title; } QString VenueRow::generateShortName() { return generateName(); } PaintRoundImageCallback VenueRow::generatePaintUserpicCallback( bool forceRound) { return [=]( QPainter &p, int x, int y, int outerWidth, int size) { _delegate->rowPaintIcon(p, x, y, size, _data.venueType); }; } class VenuesController final : public PeerListController , public VenueRowDelegate , public base::has_weak_ptr { public: VenuesController( not_null session, rpl::producer> content, Fn callback); void prepare() override; void rowClicked(not_null row) override; void rowRightActionClicked(not_null row) override; Main::Session &session() const override; void rowPaintIcon( QPainter &p, int x, int y, int size, const QString &type) override; private: struct VenueIcon { not_null document; std::shared_ptr media; uint32 paletteVersion : 31 = 0; uint32 iconLoaded : 1 = 0; QImage image; QImage icon; }; void appendRow(const VenueData &data); void rebuild(const std::vector &rows); const not_null _session; const Fn _callback; rpl::variable> _rows; base::flat_map _icons; rpl::lifetime _lifetime; }; [[nodiscard]] QString NormalizeVenuesQuery(QString query) { return query.trimmed().toLower(); } [[nodiscard]] object_ptr MakeFoursquarePromo() { auto result = object_ptr((QWidget*)nullptr); const auto skip = st::defaultVerticalListSkip; const auto raw = result.data(); raw->resize(0, skip + st::pickLocationPromoHeight); const auto shadow = CreateChild(raw); raw->widthValue() | rpl::start_with_next([=](int width) { shadow->setGeometry(0, skip, width, st::lineWidth); }, raw->lifetime()); raw->paintRequest() | rpl::start_with_next([=](QRect clip) { auto p = QPainter(raw); p.fillRect(clip, st::windowBg); p.setPen(st::windowSubTextFg); p.setFont(st::normalFont); p.drawText( raw->rect().marginsRemoved({ 0, skip, 0, 0 }), tr::lng_maps_venues_source(tr::now), style::al_center); }, raw->lifetime()); return result; } VenuesController::VenuesController( not_null session, rpl::producer> content, Fn callback) : _session(session) , _callback(std::move(callback)) , _rows(std::move(content)) { } void VenuesController::prepare() { _rows.value( ) | rpl::start_with_next([=](const std::vector &rows) { rebuild(rows); }, _lifetime); } void VenuesController::rebuild(const std::vector &rows) { auto i = 0; auto count = delegate()->peerListFullRowsCount(); while (i < rows.size()) { if (i < count) { const auto row = delegate()->peerListRowAt(i); static_cast(row.get())->update(rows[i]); } else { appendRow(rows[i]); } ++i; } while (i < count) { delegate()->peerListRemoveRow(delegate()->peerListRowAt(i)); --count; } if (i > 0) { delegate()->peerListSetBelowWidget(MakeFoursquarePromo()); } else { delegate()->peerListSetBelowWidget({ nullptr }); } delegate()->peerListRefreshRows(); } void VenuesController::rowClicked(not_null row) { _callback(static_cast(row.get())->data()); } void VenuesController::rowRightActionClicked(not_null row) { delegate()->peerListShowRowMenu(row, true); } Main::Session &VenuesController::session() const { return *_session; } void VenuesController::appendRow(const VenueData &data) { delegate()->peerListAppendRow(std::make_unique(this, data)); } void VenuesController::rowPaintIcon( QPainter &p, int x, int y, int size, const QString &icon) { auto i = _icons.find(icon); if (i == end(_icons)) { i = _icons.emplace(icon, VenueIcon{ .document = _session->data().venueIconDocument(icon), }).first; i->second.media = i->second.document->createMediaView(); i->second.document->forceToCache(true); i->second.document->save({}, QString(), LoadFromCloudOrLocal, true); } auto &data = i->second; const auto version = uint32(style::PaletteVersion()); const auto loaded = (!data.media || data.media->loaded()) ? 1 : 0; const auto prepare = data.image.isNull() || (data.iconLoaded != loaded) || (data.paletteVersion != version); if (prepare) { const auto skip = st::pickLocationIconSkip; const auto inner = size - skip * 2; const auto ratio = style::DevicePixelRatio(); if (loaded && data.media) { const auto bytes = base::take(data.media)->bytes(); data.icon = Images::Read({ .content = bytes }).image; if (!data.icon.isNull()) { data.icon = data.icon.scaled( QSize(inner, inner) * ratio, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); if (!data.icon.isNull()) { data.icon = data.icon.convertToFormat( QImage::Format_ARGB32_Premultiplied); } } } const auto full = QSize(size, size) * ratio; auto image = (data.image.size() == full) ? base::take(data.image) : QImage(full, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); image.setDevicePixelRatio(ratio); const auto bg = EmptyUserpic::UserpicColor( EmptyUserpic::ColorIndex(UniqueRowIdFromString(icon))); auto p = QPainter(&image); auto hq = PainterHighQualityEnabler(p); { auto gradient = QLinearGradient(0, 0, 0, size); gradient.setStops({ { 0., bg.color1->c }, { 1., bg.color2->c } }); p.setBrush(gradient); } p.setPen(Qt::NoPen); p.drawEllipse(QRect(0, 0, size, size)); if (!data.icon.isNull()) { p.drawImage( QRect(skip, skip, inner, inner), style::colorizeImage(data.icon, st::historyPeerUserpicFg)); } p.end(); data.paletteVersion = version; data.iconLoaded = loaded; data.image = std::move(image); } p.drawImage(x, y, data.image); } [[nodiscard]] QByteArray DefaultCenter(Core::GeoLocation initial) { const auto &use = initial.exact() ? initial : LastExactLocation; if (!use) { return "null"; } return "["_q + QByteArray::number(use.point.x()) + ","_q + QByteArray::number(use.point.y()) + "]"_q; } [[nodiscard]] QByteArray DefaultBounds() { const auto country = Core::ResolveCurrentCountryLocation(); if (!country) { return "null"; } return "[["_q + QByteArray::number(country.bounds.x()) + ","_q + QByteArray::number(country.bounds.y()) + "],["_q + QByteArray::number(country.bounds.x() + country.bounds.width()) + ","_q + QByteArray::number(country.bounds.y() + country.bounds.height()) + "]]"_q; } [[nodiscard]] QByteArray ComputeStyles() { static const auto map = base::flat_map{ { "window-bg", &st::windowBg }, { "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 }, }; return Ui::ComputeStyles(map, phrases, 100, Window::Theme::IsNightMode()); } [[nodiscard]] QByteArray ReadResource(const QString &name) { auto file = QFile(u":/picker/"_q + name); return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray(); } [[nodiscard]] QByteArray PickerContent() { return R"(
)"_q; } [[nodiscard]] object_ptr MakeChooseLocationButton( QWidget *parent, rpl::producer label, rpl::producer address) { auto result = object_ptr( parent, QString(), st::pickLocationButton); const auto raw = result.data(); const auto st = &st::pickLocationVenueItem; const auto icon = CreateChild(raw); icon->setGeometry( st->photoPosition.x(), st->photoPosition.y(), st->photoSize, st->photoSize); icon->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(icon); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::windowBgActive); p.drawEllipse(icon->rect()); st::pickLocationSendIcon.paintInCenter(p, icon->rect()); }, icon->lifetime()); icon->show(); const auto hadAddress = std::make_shared(false); auto statusText = std::move( address ) | rpl::map([=](const QString &text) { if (!text.isEmpty()) { *hadAddress = true; return text; } return *hadAddress ? tr::lng_contacts_loading(tr::now) : QString(); }); const auto name = CreateChild( raw, std::move(label), st::pickLocationButtonText); name->show(); const auto status = CreateChild( raw, rpl::duplicate(statusText), st::pickLocationButtonStatus); status->showOn(rpl::duplicate( statusText ) | rpl::map([](const QString &text) { return !text.isEmpty(); }) | rpl::distinct_until_changed()); rpl::combine( result->widthValue(), std::move(statusText) ) | rpl::start_with_next([=](int width, const QString &statusText) { const auto available = width - st->namePosition.x() - st->button.padding.right(); const auto namePosition = st->namePosition; const auto statusPosition = st->statusPosition; name->resizeToWidth(available); const auto nameTop = statusText.isEmpty() ? ((st->height - name->height()) / 2) : namePosition.y(); name->moveToLeft(namePosition.x(), nameTop, width); status->resizeToNaturalWidth(available); status->moveToLeft(statusPosition.x(), statusPosition.y(), width); }, name->lifetime()); icon->setAttribute(Qt::WA_TransparentForMouseEvents); name->setAttribute(Qt::WA_TransparentForMouseEvents); status->setAttribute(Qt::WA_TransparentForMouseEvents); return result; } void SetupLoadingView(not_null container) { class Loading final : public RpWidget { public: explicit Loading(QWidget *parent) : RpWidget(parent) , animation( [=] { if (!anim::Disabled()) update(); }, st::pickLocationLoading) { animation.start(st::pickLocationLoading.sineDuration); } private: void paintEvent(QPaintEvent *e) override { auto p = QPainter(this); const auto size = st::pickLocationLoading.size; const auto inner = QRect(QPoint(), size); const auto positioned = style::centerrect(rect(), inner); animation.draw(p, positioned.topLeft(), size, width()); } InfiniteRadialAnimation animation; }; const auto view = CreateChild(container); view->resize(container->width(), st::recentPeersEmptyHeightMin); view->show(); ResizeFitChild(container, view); } void SetupEmptyView( not_null container, std::optional query) { using Icon = Dialogs::SearchEmptyIcon; const auto view = CreateChild( container, (query ? Icon::NoResults : Icon::Search), (query ? tr::lng_maps_no_places : tr::lng_maps_choose_to_search)(Text::WithEntities)); view->setMinimalHeight(st::recentPeersEmptyHeightMin); view->show(); ResizeFitChild(container, view); InvokeQueued(view, [=] { view->animate(); }); } void SetupVenues( not_null container, std::shared_ptr show, rpl::producer value, Fn callback) { const auto otherWrap = container->add(object_ptr>( container, object_ptr(container))); const auto other = otherWrap->entity(); rpl::duplicate( value ) | rpl::start_with_next([=](const PickerVenueState &state) { while (!other->children().isEmpty()) { delete other->children()[0]; } if (v::is(state)) { otherWrap->hide(anim::type::instant); return; } else if (v::is(state)) { SetupLoadingView(other); } else { const auto n = std::get_if(&state); SetupEmptyView(other, n ? n->query : std::optional()); } otherWrap->show(anim::type::instant); }, otherWrap->lifetime()); auto &lifetime = container->lifetime(); auto venuesList = rpl::duplicate( value ) | rpl::map([=](PickerVenueState &&state) { return v::is(state) ? std::move(v::get(state).list) : std::vector(); }); const auto delegate = lifetime.make_state( show); const auto controller = lifetime.make_state( &show->session(), std::move(venuesList), std::move(callback)); controller->setStyleOverrides(&st::pickLocationVenueList); const auto content = container->add(object_ptr( container, controller)); delegate->setContent(content); controller->setDelegate(delegate); show->session().downloaderTaskFinished() | rpl::start_with_next([=] { content->update(); }, content->lifetime()); } [[nodiscard]] PickerVenueList ParseVenues( not_null session, const MTPmessages_BotResults &venues) { const auto &data = venues.data(); session->data().processUsers(data.vusers()); auto &list = data.vresults().v; auto result = PickerVenueList(); result.list.reserve(list.size()); for (const auto &found : list) { found.match([&](const auto &data) { data.vsend_message().match([&]( const MTPDbotInlineMessageMediaVenue &data) { data.vgeo().match([&](const MTPDgeoPoint &geo) { result.list.push_back({ .lat = geo.vlat().v, .lon = geo.vlong().v, .title = qs(data.vtitle()), .address = qs(data.vaddress()), .provider = qs(data.vprovider()), .id = qs(data.vvenue_id()), .venueType = qs(data.vvenue_type()), }); }, [](const auto &) {}); }, [](const auto &) {}); }); } return result; } not_null SetupMapPlaceholder( not_null parent, int minHeight, int maxHeight, Fn choose) { const auto result = CreateChild(parent); const auto top = CreateChild(result); const auto bottom = CreateChild(result); const auto icon = CreateChild(result); const auto iconSize = st::settingsCloudPasswordIconSize; auto ownedLottie = Lottie::MakeIcon({ .name = u"location"_q, .sizeOverride = { iconSize, iconSize }, .limitFps = true, }); const auto lottie = ownedLottie.get(); icon->lifetime().add([kept = std::move(ownedLottie)] {}); icon->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(icon); const auto left = (icon->width() - iconSize) / 2; const auto scale = icon->height() / float64(iconSize); auto hq = std::optional(); if (scale < 1.) { const auto center = QPointF( icon->width() / 2., icon->height() / 2.); hq.emplace(p); p.translate(center); p.scale(scale, scale); p.translate(-center); p.setOpacity(scale); } lottie->paint(p, left, 0); }, icon->lifetime()); InvokeQueued(icon, [=] { const auto till = lottie->framesCount() - 1; lottie->animate([=] { icon->update(); }, 0, till); }); const auto button = CreateChild( result, tr::lng_maps_select_on_map(), st::pickLocationChooseOnMap); button->setFullRadius(true); button->setTextTransform(RoundButton::TextTransform::NoTransform); button->setClickedCallback(choose); parent->sizeValue() | rpl::start_with_next([=](QSize size) { result->setGeometry(QRect(QPoint(), size)); const auto width = size.width(); top->setGeometry(0, 0, width, top->height()); bottom->setGeometry(QRect( QPoint(0, size.height() - bottom->height()), QSize(width, bottom->height()))); const auto dividers = top->height() + bottom->height(); const auto ratio = (size.height() - minHeight) / float64(maxHeight - minHeight); const auto iconHeight = int(base::SafeRound(ratio * iconSize)); const auto available = size.height() - dividers; const auto maxDelta = (maxHeight - dividers - iconSize - button->height()) / 2; const auto minDelta = (minHeight - dividers - button->height()) / 2; const auto delta = anim::interpolate(minDelta, maxDelta, ratio); button->move( (width - button->width()) / 2, size.height() - bottom->height() - delta - button->height()); const auto wide = available - delta - button->height(); const auto skip = (wide - iconHeight) / 2; icon->setGeometry(0, top->height() + skip, width, iconHeight); }, result->lifetime()); top->show(); icon->show(); bottom->show(); result->show(); return result; } } // namespace LocationPicker::LocationPicker(Descriptor &&descriptor) : _config(std::move(descriptor.config)) , _callback(std::move(descriptor.callback)) , _quit(std::move(descriptor.quit)) , _window(std::make_unique()) , _body((_window->setInnerSize(st::pickLocationWindow) , _window->showInner(base::make_unique_q(_window.get())) , _window->inner())) , _chooseButtonLabel(std::move(descriptor.chooseLabel)) , _webviewStorageId(descriptor.storageId) , _updateStyles([=] { const auto str = EscapeForScriptString(ComputeStyles()); if (_webview) { _webview->eval("LocationPicker.updateStyles('" + str + "');"); } }) , _geocoderResolveTimer([=] { resolveAddressByTimer(); }) , _venueState(PickerVenueLoading()) , _session(descriptor.session) , _venuesSearchDebounceTimer([=] { Expects(_venuesSearchLocation.has_value()); Expects(_venuesSearchQuery.has_value()); venuesRequest(*_venuesSearchLocation, *_venuesSearchQuery); }) , _api(&_session->mtp()) , _venueRecipient(descriptor.recipient) { std::move( descriptor.closeRequests ) | rpl::start_with_next([=] { _window = nullptr; delete this; }, _lifetime); setup(descriptor); } std::shared_ptr LocationPicker::uiShow() { return Main::MakeSessionShow(nullptr, _session); } bool LocationPicker::Available(const LocationPickerConfig &config) { static const auto Supported = Webview::NavigateToDataSupported(); return Supported && !config.mapsToken.isEmpty(); } void LocationPicker::setup(const Descriptor &descriptor) { setupWindow(descriptor); _initialProvided = descriptor.initial; const auto initial = _initialProvided.exact() ? _initialProvided : LastExactLocation; if (initial) { venuesRequest(initial); resolveAddress(initial); venuesSearchEnableAt(initial); } if (!_initialProvided) { resolveCurrentLocation(); } } void LocationPicker::setupWindow(const Descriptor &descriptor) { const auto window = _window.get(); window->setWindowFlag(Qt::WindowStaysOnTopHint, false); window->closeRequests() | rpl::start_with_next([=] { close(); }, _lifetime); const auto parent = descriptor.parent ? descriptor.parent->window()->geometry() : QGuiApplication::primaryScreen()->availableGeometry(); window->setTitle(tr::lng_maps_point()); window->move( parent.x() + (parent.width() - window->width()) / 2, parent.y() + (parent.height() - window->height()) / 2); _container = CreateChild(_body.get()); _mapPlaceholderAdded = st::pickLocationButtonSkip + st::pickLocationButton.height + st::pickLocationButtonSkip + st::boxDividerHeight; const auto min = st::pickLocationCollapsedHeight + _mapPlaceholderAdded; const auto max = st::pickLocationMapHeight + _mapPlaceholderAdded; _mapPlaceholder = SetupMapPlaceholder(_container, min, max, [=] { setupWebview(); }); _scroll = CreateChild(_body.get()); const auto controls = _scroll->setOwnedWidget( object_ptr(_scroll)); _mapControlsWrap = controls->add( object_ptr>( controls, object_ptr(controls))); _mapControlsWrap->show(anim::type::instant); const auto mapControls = _mapControlsWrap->entity(); const auto toppad = mapControls->add(object_ptr(controls)); AddSkip(mapControls); AddSubsectionTitle(mapControls, tr::lng_maps_or_choose()); auto state = _venueState.value(); SetupVenues(controls, uiShow(), std::move(state), [=](VenueData info) { _callback(std::move(info)); close(); }); rpl::combine( _body->sizeValue(), _scroll->scrollTopValue(), _venuesSearchShown.value() ) | rpl::start_with_next([=](QSize size, int scrollTop, bool search) { const auto width = size.width(); const auto height = size.height(); const auto sub = std::min( (st::pickLocationMapHeight - st::pickLocationCollapsedHeight), scrollTop); const auto mapHeight = st::pickLocationMapHeight - sub + (_mapPlaceholder ? _mapPlaceholderAdded : 0); _container->setGeometry(0, 0, width, mapHeight); const auto scrollWidgetTop = search ? 0 : mapHeight; const auto scrollHeight = height - scrollWidgetTop; _scroll->setGeometry(0, scrollWidgetTop, width, scrollHeight); controls->resizeToWidth(width); toppad->resize(width, sub); }, _container->lifetime()); _container->paintRequest() | rpl::start_with_next([=](QRect clip) { QPainter(_container).fillRect(clip, st::windowBg); }, _container->lifetime()); _container->show(); _scroll->show(); controls->show(); window->show(); } void LocationPicker::setupWebview() { Expects(!_webview); delete base::take(_mapPlaceholder); const auto mapControls = _mapControlsWrap->entity(); mapControls->insert( 1, object_ptr(mapControls) )->show(); _mapButton = mapControls->insert( 1, MakeChooseLocationButton( mapControls, _chooseButtonLabel.value(), _geocoderAddress.value()), { 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip }); _mapButton->setClickedCallback([=] { _webview->eval("LocationPicker.send();"); }); _mapButton->hide(); _scroll->scrollToY(0); _venuesSearchShown.force_assign(_venuesSearchShown.current()); _mapLoading = CreateChild(_body.get()); _container->geometryValue() | rpl::start_with_next([=](QRect rect) { _mapLoading->setGeometry(rect); }, _mapLoading->lifetime()); SetupLoadingView(_mapLoading); _mapLoading->show(); const auto window = _window.get(); _webview = std::make_unique( _container, Webview::WindowConfig{ .opaqueBg = st::windowBg->c, .storageId = _webviewStorageId, .dataProtocolOverride = kProtocolOverride, }); const auto raw = _webview.get(); window->lifetime().add([=] { _webview = nullptr; }); window->events( ) | rpl::start_with_next([=](not_null e) { if (e->type() == QEvent::Close) { close(); } else if (e->type() == QEvent::KeyPress) { const auto event = static_cast(e.get()); if (event->key() == Qt::Key_Escape && !_venuesSearchQuery) { close(); } } }, window->lifetime()); raw->widget()->show(); _container->sizeValue( ) | rpl::start_with_next([=](QSize size) { raw->widget()->setGeometry(QRect(QPoint(), size)); }, _container->lifetime()); raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { return true; }); raw->setNavigationDoneHandler([=](bool success) { }); raw->setMessageHandler([=](const QJsonDocument &message) { crl::on_main(_window.get(), [=] { const auto object = message.object(); const auto event = object.value("event").toString(); if (event == u"ready"_q) { mapReady(); } else if (event == u"keydown"_q) { const auto key = object.value("key").toString(); const auto modifier = object.value("modifier").toString(); processKey(key, modifier); } else if (event == u"send"_q) { const auto lat = object.value("latitude").toDouble(); const auto lon = object.value("longitude").toDouble(); _callback({ .lat = lat, .lon = lon, .address = _geocoderAddress.current(), }); close(); } 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"move_end"_q) { const auto lat = object.value("latitude").toDouble(); const auto lon = object.value("longitude").toDouble(); const auto location = Core::GeoLocation{ .point = { lat, lon }, .accuracy = Core::GeoLocationAccuracy::Exact, }; 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);"); } venuesSearchEnableAt(location); } 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, }); } }); }); raw->setDataRequestHandler([=](Webview::DataRequest request) { const auto pos = request.id.find('#'); if (pos != request.id.npos) { request.id = request.id.substr(0, pos); } if (!request.id.starts_with("location/")) { return Webview::DataResult::Failed; } const auto finishWith = [&](QByteArray data, std::string mime) { request.done({ .stream = std::make_unique( std::move(data), std::move(mime)), }); return Webview::DataResult::Done; }; if (!_subscribedToColors) { _subscribedToColors = true; rpl::merge( Lang::Updated(), style::PaletteChanged() ) | rpl::start_with_next([=] { _updateStyles.call(); }, _webview->lifetime()); } const auto id = std::string_view(request.id).substr(9); if (id == "picker.html") { return finishWith(PickerContent(), "text/html; charset=utf-8"); } const auto css = id.ends_with(".css"); const auto js = !css && id.ends_with(".js"); if (!css && !js) { return Webview::DataResult::Failed; } const auto qstring = QString::fromUtf8(id.data(), id.size()); const auto pattern = u"^[a-zA-Z\\.\\-_0-9]+$"_q; if (QRegularExpression(pattern).match(qstring).hasMatch()) { const auto bytes = ReadResource(qstring); if (!bytes.isEmpty()) { const auto mime = css ? "text/css" : "text/javascript"; return finishWith(bytes, mime); } } return Webview::DataResult::Failed; }); raw->init(R"()"); raw->navigateToData("location/picker.html"); } void LocationPicker::resolveAddressByTimer() { if (const auto location = base::take(_geocoderResolvePostponed)) { resolveAddress(location); } } void LocationPicker::resolveAddress(Core::GeoLocation location) { if (AreTheSame(_geocoderResolvingFor, location)) { return; } _geocoderResolvingFor = location; const auto done = [=](Core::GeoAddress address) { if (!AreTheSame(_geocoderResolvingFor, location)) { return; } else if (address) { _geocoderAddress = address.name; } else { _geocoderAddress = u"(%1, %2)"_q .arg(location.point.x(), 0, 'f') .arg(location.point.y(), 0, 'f'); } }; const auto baseLangId = Lang::GetInstance().baseId(); const auto langId = baseLangId.isEmpty() ? Lang::GetInstance().id() : baseLangId; const auto nonEmptyId = langId.isEmpty() ? u"en"_q : langId; Core::ResolveLocationAddress( location, langId, _config.geoToken, crl::guard(this, done)); } void LocationPicker::mapReady() { Expects(_scroll != nullptr); delete base::take(_mapLoading); const auto token = _config.mapsToken.toUtf8(); const auto center = DefaultCenter(_initialProvided); const auto bounds = DefaultBounds(); const auto protocol = *kProtocolOverride ? "'"_q + kProtocolOverride + "'" : "null"; const auto params = "token: '" + token + "'" + ", center: " + center + ", bounds: " + bounds + ", protocol: " + protocol; _webview->eval("LocationPicker.init({ " + params + " });"); const auto handle = _window->window()->windowHandle(); if (handle && QGuiApplication::focusWindow() == handle) { _webview->focus(); } _mapButton->show(); } bool LocationPicker::venuesFromCache( Core::GeoLocation location, QString query) { const auto normalized = NormalizeVenuesQuery(query); auto &cache = _venuesCache[normalized]; const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) { return AreTheSame(v.location, location); }); if (i == end(cache)) { return false; } _venuesRequestLocation = location; _venuesRequestQuery = normalized; _venuesInitialQuery = query; venuesApplyResults(i->result); return true; } void LocationPicker::venuesRequest( Core::GeoLocation location, QString query) { const auto normalized = NormalizeVenuesQuery(query); if (AreTheSame(_venuesRequestLocation, location) && _venuesRequestQuery == normalized) { return; } else if (const auto oldRequestId = base::take(_venuesRequestId)) { _api.request(oldRequestId).cancel(); } _venueState = PickerVenueLoading(); _venuesRequestLocation = location; _venuesRequestQuery = normalized; _venuesInitialQuery = query; if (_venuesBot) { venuesSendRequest(); } else if (_venuesBotRequestId) { return; } const auto username = _session->serverConfig().venueSearchUsername; _venuesBotRequestId = _api.request(MTPcontacts_ResolveUsername( MTP_flags(0), MTP_string(username), MTP_string() )).done([=](const MTPcontacts_ResolvedPeer &result) { auto &data = result.data(); _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); const auto peer = _session->data().peerLoaded( peerFromMTP(data.vpeer())); const auto user = peer ? peer->asUser() : nullptr; if (user && user->isBotInlineGeo()) { _venuesBot = user; venuesSendRequest(); } else { LOG(("API Error: Bad peer returned by: %1").arg(username)); } }).fail([=] { LOG(("API Error: Error returned on lookup: %1").arg(username)); }).send(); } void LocationPicker::venuesSendRequest() { Expects(_venuesBot != nullptr); if (_venuesRequestId || !_venuesRequestLocation) { return; } _venuesRequestId = _api.request(MTPmessages_GetInlineBotResults( MTP_flags(MTPmessages_GetInlineBotResults::Flag::f_geo_point), _venuesBot->inputUser, (_venueRecipient ? _venueRecipient->input : MTP_inputPeerEmpty()), MTP_inputGeoPoint( MTP_flags(0), MTP_double(_venuesRequestLocation.point.x()), MTP_double(_venuesRequestLocation.point.y()), MTP_int(0)), // accuracy_radius MTP_string(_venuesRequestQuery), MTP_string() // offset )).done([=](const MTPmessages_BotResults &result) { auto parsed = ParseVenues(_session, result); _venuesCache[_venuesRequestQuery].push_back({ .location = _venuesRequestLocation, .result = parsed, }); venuesApplyResults(std::move(parsed)); }).fail([=] { venuesApplyResults({}); }).send(); } void LocationPicker::venuesApplyResults(PickerVenueList venues) { _venuesRequestId = 0; if (venues.list.empty()) { _venueState = PickerVenueNothingFound{ _venuesInitialQuery }; } else { _venueState = std::move(venues); } } void LocationPicker::venuesSearchEnableAt(Core::GeoLocation location) { if (!_venuesSearchLocation) { _window->setSearchAllowed( tr::lng_dlg_filter(), [=](std::optional query) { venuesSearchChanged(query); }); } _venuesSearchLocation = location; } void LocationPicker::venuesSearchChanged( const std::optional &query) { _venuesSearchQuery = query; const auto shown = query && !query->trimmed().isEmpty(); _venuesSearchShown = shown; if (_container->isHidden() != shown) { _container->setVisible(!shown); _mapControlsWrap->toggle(!shown, anim::type::instant); if (shown) { _venuesNoSearchLocation = _venuesRequestLocation; } else if (_venuesNoSearchLocation) { if (!venuesFromCache(_venuesNoSearchLocation)) { venuesRequest(_venuesNoSearchLocation); } } } if (shown && !venuesFromCache( *_venuesSearchLocation, *_venuesSearchQuery)) { _venueState = PickerVenueLoading(); _venuesSearchDebounceTimer.callOnce(kSearchDebounceDelay); } else { _venuesSearchDebounceTimer.cancel(); } } void LocationPicker::resolveCurrentLocation() { using namespace Core; const auto window = _window.get(); ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) { const auto changed = !AreTheSame(LastExactLocation, location); if (location.accuracy != GeoLocationAccuracy::Exact || !changed) { if (!_venuesSearchLocation) { _venueState = PickerVenueWaitingForLocation(); } return; } LastExactLocation = location; if (location) { if (_venuesSearchQuery.value_or(QString()).isEmpty()) { venuesRequest(location); } resolveAddress(location); } if (_webview) { const auto point = QByteArray::number(location.point.x()) + ","_q + QByteArray::number(location.point.y()); _webview->eval("LocationPicker.narrowTo([" + point + "]);"); } })); } void LocationPicker::processKey( const QString &key, const QString &modifier) { const auto ctrl = ::Platform::IsMac() ? u"cmd"_q : u"ctrl"_q; if (key == u"escape"_q) { if (!_window->closeSearch()) { close(); } } else if (key == u"w"_q && modifier == ctrl) { close(); } else if (key == u"m"_q && modifier == ctrl) { minimize(); } else if (key == u"q"_q && modifier == ctrl) { quit(); } } void LocationPicker::activate() { if (_window) { _window->activateWindow(); } } void LocationPicker::close() { crl::on_main(this, [=] { _window = nullptr; delete this; }); } void LocationPicker::minimize() { if (_window) { _window->setWindowState(_window->windowState() | Qt::WindowMinimized); } } void LocationPicker::quit() { if (const auto onstack = _quit) { onstack(); } } not_null LocationPicker::Show(Descriptor &&descriptor) { return new LocationPicker(std::move(descriptor)); } } // namespace Ui