mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-14 13:17:08 +02:00
1335 lines
38 KiB
C++
1335 lines
38 KiB
C++
/*
|
|
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 <QtCore/QFile>
|
|
#include <QtCore/QJsonDocument>
|
|
#include <QtCore/QJsonObject>
|
|
#include <QtCore/QJsonValue>
|
|
#include <QtGui/QGuiApplication>
|
|
#include <QtGui/QScreen>
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
constexpr auto kResolveAddressDelay = 3 * crl::time(1000);
|
|
constexpr auto kSearchDebounceDelay = crl::time(900);
|
|
|
|
#if defined Q_OS_MAC || defined Q_OS_LINUX
|
|
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<VenueRowDelegate*> 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<VenueRowDelegate*> _delegate;
|
|
VenueData _data;
|
|
|
|
};
|
|
|
|
VenueRow::VenueRow(
|
|
not_null<VenueRowDelegate*> 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<Main::Session*> session,
|
|
rpl::producer<std::vector<VenueData>> content,
|
|
Fn<void(VenueData)> callback);
|
|
|
|
void prepare() override;
|
|
void rowClicked(not_null<PeerListRow*> row) override;
|
|
void rowRightActionClicked(not_null<PeerListRow*> 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<DocumentData*> document;
|
|
std::shared_ptr<Data::DocumentMedia> media;
|
|
uint32 paletteVersion : 31 = 0;
|
|
uint32 iconLoaded : 1 = 0;
|
|
QImage image;
|
|
QImage icon;
|
|
};
|
|
|
|
void appendRow(const VenueData &data);
|
|
|
|
void rebuild(const std::vector<VenueData> &rows);
|
|
|
|
const not_null<Main::Session*> _session;
|
|
const Fn<void(VenueData)> _callback;
|
|
rpl::variable<std::vector<VenueData>> _rows;
|
|
|
|
base::flat_map<QString, VenueIcon> _icons;
|
|
|
|
rpl::lifetime _lifetime;
|
|
|
|
};
|
|
|
|
[[nodiscard]] QString NormalizeVenuesQuery(QString query) {
|
|
return query.trimmed().toLower();
|
|
}
|
|
|
|
[[nodiscard]] object_ptr<RpWidget> MakeFoursquarePromo() {
|
|
auto result = object_ptr<RpWidget>((QWidget*)nullptr);
|
|
const auto skip = st::defaultVerticalListSkip;
|
|
const auto raw = result.data();
|
|
raw->resize(0, skip + st::pickLocationPromoHeight);
|
|
const auto shadow = CreateChild<PlainShadow>(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<Main::Session*> session,
|
|
rpl::producer<std::vector<VenueData>> content,
|
|
Fn<void(VenueData)> callback)
|
|
: _session(session)
|
|
, _callback(std::move(callback))
|
|
, _rows(std::move(content)) {
|
|
}
|
|
|
|
void VenuesController::prepare() {
|
|
_rows.value(
|
|
) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
|
|
rebuild(rows);
|
|
}, _lifetime);
|
|
}
|
|
|
|
void VenuesController::rebuild(const std::vector<VenueData> &rows) {
|
|
auto i = 0;
|
|
auto count = delegate()->peerListFullRowsCount();
|
|
while (i < rows.size()) {
|
|
if (i < count) {
|
|
const auto row = delegate()->peerListRowAt(i);
|
|
static_cast<VenueRow*>(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<PeerListRow*> row) {
|
|
_callback(static_cast<VenueRow*>(row.get())->data());
|
|
}
|
|
|
|
void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
|
|
delegate()->peerListShowRowMenu(row, true);
|
|
}
|
|
|
|
Main::Session &VenuesController::session() const {
|
|
return *_session;
|
|
}
|
|
|
|
void VenuesController::appendRow(const VenueData &data) {
|
|
delegate()->peerListAppendRow(std::make_unique<VenueRow>(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<QByteArray, const style::color*>{
|
|
{ "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<QByteArray, tr::phrase<>>{
|
|
{ "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"(<!DOCTYPE html>
|
|
<html style=")"
|
|
+ EscapeForAttribute(ComputeStyles())
|
|
+ R"(">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<script src="/location/picker.js"></script>
|
|
<link rel="stylesheet" href="/location/picker.css" />
|
|
<script src='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.js'></script>
|
|
<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">
|
|
<defs>
|
|
<radialGradient id="shadowGradient">
|
|
<stop offset="10%" stop-opacity="0.4"></stop>
|
|
<stop offset="100%" stop-opacity="0.05"></stop>
|
|
</radialGradient>
|
|
</defs>
|
|
<ellipse
|
|
cx="13.5"
|
|
cy="34.8"
|
|
rx="10.5"
|
|
ry="5.25"
|
|
fill=")" + "url(#shadowGradient)" + R"("></ellipse>
|
|
</svg>
|
|
</div>
|
|
<div id="marker_drop" style="transform: translate(0px, -14px);">
|
|
<svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
|
|
<path fill="#3FB1CE" d="M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z"></path><path opacity="0.25" d="M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z"></path>
|
|
<circle fill="white" cx="13.5" cy="13.5" r="5.5"></circle>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div id="map"></div>
|
|
<script>LocationPicker.notify({ event: 'ready' });</script>
|
|
</body>
|
|
</html>
|
|
)"_q;
|
|
}
|
|
|
|
[[nodiscard]] object_ptr<AbstractButton> MakeChooseLocationButton(
|
|
QWidget *parent,
|
|
rpl::producer<QString> label,
|
|
rpl::producer<QString> address) {
|
|
auto result = object_ptr<FlatButton>(
|
|
parent,
|
|
QString(),
|
|
st::pickLocationButton);
|
|
const auto raw = result.data();
|
|
|
|
const auto st = &st::pickLocationVenueItem;
|
|
const auto icon = CreateChild<RpWidget>(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<bool>(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<FlatLabel>(
|
|
raw,
|
|
std::move(label),
|
|
st::pickLocationButtonText);
|
|
name->show();
|
|
const auto status = CreateChild<FlatLabel>(
|
|
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<RpWidget*> 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<Loading>(container);
|
|
view->resize(container->width(), st::recentPeersEmptyHeightMin);
|
|
view->show();
|
|
|
|
ResizeFitChild(container, view);
|
|
}
|
|
|
|
void SetupEmptyView(
|
|
not_null<RpWidget*> container,
|
|
std::optional<QString> query) {
|
|
using Icon = Dialogs::SearchEmptyIcon;
|
|
const auto view = CreateChild<Dialogs::SearchEmpty>(
|
|
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<VerticalLayout*> container,
|
|
std::shared_ptr<Main::SessionShow> show,
|
|
rpl::producer<PickerVenueState> value,
|
|
Fn<void(VenueData)> callback) {
|
|
const auto otherWrap = container->add(object_ptr<SlideWrap<RpWidget>>(
|
|
container,
|
|
object_ptr<RpWidget>(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<PickerVenueList>(state)) {
|
|
otherWrap->hide(anim::type::instant);
|
|
return;
|
|
} else if (v::is<PickerVenueLoading>(state)) {
|
|
SetupLoadingView(other);
|
|
} else {
|
|
const auto n = std::get_if<PickerVenueNothingFound>(&state);
|
|
SetupEmptyView(other, n ? n->query : std::optional<QString>());
|
|
}
|
|
otherWrap->show(anim::type::instant);
|
|
}, otherWrap->lifetime());
|
|
|
|
auto &lifetime = container->lifetime();
|
|
auto venuesList = rpl::duplicate(
|
|
value
|
|
) | rpl::map([=](PickerVenueState &&state) {
|
|
return v::is<PickerVenueList>(state)
|
|
? std::move(v::get<PickerVenueList>(state).list)
|
|
: std::vector<VenueData>();
|
|
});
|
|
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
|
|
show);
|
|
const auto controller = lifetime.make_state<VenuesController>(
|
|
&show->session(),
|
|
std::move(venuesList),
|
|
std::move(callback));
|
|
controller->setStyleOverrides(&st::pickLocationVenueList);
|
|
const auto content = container->add(object_ptr<PeerListContent>(
|
|
container,
|
|
controller));
|
|
delegate->setContent(content);
|
|
controller->setDelegate(delegate);
|
|
|
|
show->session().downloaderTaskFinished() | rpl::start_with_next([=] {
|
|
content->update();
|
|
}, content->lifetime());
|
|
}
|
|
|
|
[[nodiscard]] PickerVenueList ParseVenues(
|
|
not_null<Main::Session*> 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<RpWidget*> SetupMapPlaceholder(
|
|
not_null<RpWidget*> parent,
|
|
int minHeight,
|
|
int maxHeight,
|
|
Fn<void()> choose) {
|
|
const auto result = CreateChild<RpWidget>(parent);
|
|
|
|
const auto top = CreateChild<BoxContentDivider>(result);
|
|
const auto bottom = CreateChild<BoxContentDivider>(result);
|
|
|
|
const auto icon = CreateChild<RpWidget>(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<PainterHighQualityEnabler>();
|
|
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<RoundButton>(
|
|
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<SeparatePanel>())
|
|
, _body((_window->setInnerSize(st::pickLocationWindow)
|
|
, _window->showInner(base::make_unique_q<RpWidget>(_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<Main::SessionShow> LocationPicker::uiShow() {
|
|
return Main::MakeSessionShow(nullptr, _session);
|
|
}
|
|
|
|
bool LocationPicker::Available(const LocationPickerConfig &config) {
|
|
static const auto Supported = [&] {
|
|
const auto availability = Webview::Availability();
|
|
return availability.customSchemeRequests
|
|
&& availability.customReferer;
|
|
}();
|
|
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<RpWidget>(_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<ScrollArea>(_body.get());
|
|
const auto controls = _scroll->setOwnedWidget(
|
|
object_ptr<VerticalLayout>(_scroll));
|
|
|
|
_mapControlsWrap = controls->add(
|
|
object_ptr<SlideWrap<VerticalLayout>>(
|
|
controls,
|
|
object_ptr<VerticalLayout>(controls)));
|
|
_mapControlsWrap->show(anim::type::instant);
|
|
const auto mapControls = _mapControlsWrap->entity();
|
|
|
|
const auto toppad = mapControls->add(object_ptr<RpWidget>(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<BoxContentDivider>(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<RpWidget>(_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<Webview::Window>(
|
|
_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<QEvent*> e) {
|
|
if (e->type() == QEvent::Close) {
|
|
close();
|
|
} else if (e->type() == QEvent::KeyPress) {
|
|
const auto event = static_cast<QKeyEvent*>(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<Webview::DataStreamFromMemory>(
|
|
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<QString> query) {
|
|
venuesSearchChanged(query);
|
|
});
|
|
}
|
|
_venuesSearchLocation = location;
|
|
}
|
|
|
|
void LocationPicker::venuesSearchChanged(
|
|
const std::optional<QString> &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*> LocationPicker::Show(Descriptor &&descriptor) {
|
|
return new LocationPicker(std::move(descriptor));
|
|
}
|
|
|
|
} // namespace Ui
|