location_picker.cpp 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "ui/controls/location_picker.h"
  8. #include "apiwrap.h"
  9. #include "base/platform/base_platform_info.h"
  10. #include "boxes/peer_list_box.h"
  11. #include "core/current_geo_location.h"
  12. #include "data/data_document.h"
  13. #include "data/data_document_media.h"
  14. #include "data/data_file_origin.h"
  15. #include "data/data_location.h"
  16. #include "data/data_session.h"
  17. #include "data/data_user.h"
  18. #include "dialogs/ui/chat_search_empty.h" // Dialogs::SearchEmpty.
  19. #include "lang/lang_instance.h"
  20. #include "lang/lang_keys.h"
  21. #include "lottie/lottie_icon.h"
  22. #include "main/session/session_show.h"
  23. #include "main/main_session.h"
  24. #include "mtproto/mtproto_config.h"
  25. #include "ui/effects/radial_animation.h"
  26. #include "ui/text/text_utilities.h"
  27. #include "ui/widgets/scroll_area.h"
  28. #include "ui/widgets/separate_panel.h"
  29. #include "ui/widgets/shadow.h"
  30. #include "ui/widgets/buttons.h"
  31. #include "ui/wrap/slide_wrap.h"
  32. #include "ui/wrap/vertical_layout.h"
  33. #include "ui/painter.h"
  34. #include "ui/vertical_list.h"
  35. #include "ui/webview_helpers.h"
  36. #include "webview/webview_data_stream_memory.h"
  37. #include "webview/webview_embed.h"
  38. #include "webview/webview_interface.h"
  39. #include "window/themes/window_theme.h"
  40. #include "styles/style_chat_helpers.h"
  41. #include "styles/style_dialogs.h"
  42. #include "styles/style_window.h"
  43. #include "styles/style_settings.h" // settingsCloudPasswordIconSize
  44. #include "styles/style_layers.h" // boxDividerHeight
  45. #include <QtCore/QFile>
  46. #include <QtCore/QJsonDocument>
  47. #include <QtCore/QJsonObject>
  48. #include <QtCore/QJsonValue>
  49. #include <QtGui/QGuiApplication>
  50. #include <QtGui/QScreen>
  51. namespace Ui {
  52. namespace {
  53. constexpr auto kResolveAddressDelay = 3 * crl::time(1000);
  54. constexpr auto kSearchDebounceDelay = crl::time(900);
  55. #if defined Q_OS_MAC || defined Q_OS_LINUX
  56. const auto kProtocolOverride = "mapboxapihelper";
  57. #else // Q_OS_MAC
  58. const auto kProtocolOverride = "";
  59. #endif // Q_OS_MAC
  60. Core::GeoLocation LastExactLocation;
  61. using VenueData = Data::InputVenue;
  62. class VenueRowDelegate {
  63. public:
  64. virtual void rowPaintIcon(
  65. QPainter &p,
  66. int x,
  67. int y,
  68. int size,
  69. const QString &type) = 0;
  70. };
  71. class VenueRow final : public PeerListRow {
  72. public:
  73. VenueRow(not_null<VenueRowDelegate*> delegate, const VenueData &data);
  74. void update(const VenueData &data);
  75. [[nodiscard]] VenueData data() const;
  76. QString generateName() override;
  77. QString generateShortName() override;
  78. PaintRoundImageCallback generatePaintUserpicCallback(
  79. bool forceRound) override;
  80. private:
  81. const not_null<VenueRowDelegate*> _delegate;
  82. VenueData _data;
  83. };
  84. VenueRow::VenueRow(
  85. not_null<VenueRowDelegate*> delegate,
  86. const VenueData &data)
  87. : PeerListRow(UniqueRowIdFromString(data.id))
  88. , _delegate(delegate)
  89. , _data(data) {
  90. setCustomStatus(data.address);
  91. }
  92. void VenueRow::update(const VenueData &data) {
  93. _data = data;
  94. setCustomStatus(data.address);
  95. refreshName(st::pickLocationVenueItem);
  96. }
  97. VenueData VenueRow::data() const {
  98. return _data;
  99. }
  100. QString VenueRow::generateName() {
  101. return _data.title;
  102. }
  103. QString VenueRow::generateShortName() {
  104. return generateName();
  105. }
  106. PaintRoundImageCallback VenueRow::generatePaintUserpicCallback(
  107. bool forceRound) {
  108. return [=](
  109. QPainter &p,
  110. int x,
  111. int y,
  112. int outerWidth,
  113. int size) {
  114. _delegate->rowPaintIcon(p, x, y, size, _data.venueType);
  115. };
  116. }
  117. class VenuesController final
  118. : public PeerListController
  119. , public VenueRowDelegate
  120. , public base::has_weak_ptr {
  121. public:
  122. VenuesController(
  123. not_null<Main::Session*> session,
  124. rpl::producer<std::vector<VenueData>> content,
  125. Fn<void(VenueData)> callback);
  126. void prepare() override;
  127. void rowClicked(not_null<PeerListRow*> row) override;
  128. void rowRightActionClicked(not_null<PeerListRow*> row) override;
  129. Main::Session &session() const override;
  130. void rowPaintIcon(
  131. QPainter &p,
  132. int x,
  133. int y,
  134. int size,
  135. const QString &type) override;
  136. private:
  137. struct VenueIcon {
  138. not_null<DocumentData*> document;
  139. std::shared_ptr<Data::DocumentMedia> media;
  140. uint32 paletteVersion : 31 = 0;
  141. uint32 iconLoaded : 1 = 0;
  142. QImage image;
  143. QImage icon;
  144. };
  145. void appendRow(const VenueData &data);
  146. void rebuild(const std::vector<VenueData> &rows);
  147. const not_null<Main::Session*> _session;
  148. const Fn<void(VenueData)> _callback;
  149. rpl::variable<std::vector<VenueData>> _rows;
  150. base::flat_map<QString, VenueIcon> _icons;
  151. rpl::lifetime _lifetime;
  152. };
  153. [[nodiscard]] QString NormalizeVenuesQuery(QString query) {
  154. return query.trimmed().toLower();
  155. }
  156. [[nodiscard]] object_ptr<RpWidget> MakeFoursquarePromo() {
  157. auto result = object_ptr<RpWidget>((QWidget*)nullptr);
  158. const auto skip = st::defaultVerticalListSkip;
  159. const auto raw = result.data();
  160. raw->resize(0, skip + st::pickLocationPromoHeight);
  161. const auto shadow = CreateChild<PlainShadow>(raw);
  162. raw->widthValue() | rpl::start_with_next([=](int width) {
  163. shadow->setGeometry(0, skip, width, st::lineWidth);
  164. }, raw->lifetime());
  165. raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
  166. auto p = QPainter(raw);
  167. p.fillRect(clip, st::windowBg);
  168. p.setPen(st::windowSubTextFg);
  169. p.setFont(st::normalFont);
  170. p.drawText(
  171. raw->rect().marginsRemoved({ 0, skip, 0, 0 }),
  172. tr::lng_maps_venues_source(tr::now),
  173. style::al_center);
  174. }, raw->lifetime());
  175. return result;
  176. }
  177. VenuesController::VenuesController(
  178. not_null<Main::Session*> session,
  179. rpl::producer<std::vector<VenueData>> content,
  180. Fn<void(VenueData)> callback)
  181. : _session(session)
  182. , _callback(std::move(callback))
  183. , _rows(std::move(content)) {
  184. }
  185. void VenuesController::prepare() {
  186. _rows.value(
  187. ) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
  188. rebuild(rows);
  189. }, _lifetime);
  190. }
  191. void VenuesController::rebuild(const std::vector<VenueData> &rows) {
  192. auto i = 0;
  193. auto count = delegate()->peerListFullRowsCount();
  194. while (i < rows.size()) {
  195. if (i < count) {
  196. const auto row = delegate()->peerListRowAt(i);
  197. static_cast<VenueRow*>(row.get())->update(rows[i]);
  198. } else {
  199. appendRow(rows[i]);
  200. }
  201. ++i;
  202. }
  203. while (i < count) {
  204. delegate()->peerListRemoveRow(delegate()->peerListRowAt(i));
  205. --count;
  206. }
  207. if (i > 0) {
  208. delegate()->peerListSetBelowWidget(MakeFoursquarePromo());
  209. } else {
  210. delegate()->peerListSetBelowWidget({ nullptr });
  211. }
  212. delegate()->peerListRefreshRows();
  213. }
  214. void VenuesController::rowClicked(not_null<PeerListRow*> row) {
  215. _callback(static_cast<VenueRow*>(row.get())->data());
  216. }
  217. void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
  218. delegate()->peerListShowRowMenu(row, true);
  219. }
  220. Main::Session &VenuesController::session() const {
  221. return *_session;
  222. }
  223. void VenuesController::appendRow(const VenueData &data) {
  224. delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));
  225. }
  226. void VenuesController::rowPaintIcon(
  227. QPainter &p,
  228. int x,
  229. int y,
  230. int size,
  231. const QString &icon) {
  232. auto i = _icons.find(icon);
  233. if (i == end(_icons)) {
  234. i = _icons.emplace(icon, VenueIcon{
  235. .document = _session->data().venueIconDocument(icon),
  236. }).first;
  237. i->second.media = i->second.document->createMediaView();
  238. i->second.document->forceToCache(true);
  239. i->second.document->save({}, QString(), LoadFromCloudOrLocal, true);
  240. }
  241. auto &data = i->second;
  242. const auto version = uint32(style::PaletteVersion());
  243. const auto loaded = (!data.media || data.media->loaded()) ? 1 : 0;
  244. const auto prepare = data.image.isNull()
  245. || (data.iconLoaded != loaded)
  246. || (data.paletteVersion != version);
  247. if (prepare) {
  248. const auto skip = st::pickLocationIconSkip;
  249. const auto inner = size - skip * 2;
  250. const auto ratio = style::DevicePixelRatio();
  251. if (loaded && data.media) {
  252. const auto bytes = base::take(data.media)->bytes();
  253. data.icon = Images::Read({ .content = bytes }).image;
  254. if (!data.icon.isNull()) {
  255. data.icon = data.icon.scaled(
  256. QSize(inner, inner) * ratio,
  257. Qt::IgnoreAspectRatio,
  258. Qt::SmoothTransformation);
  259. if (!data.icon.isNull()) {
  260. data.icon = data.icon.convertToFormat(
  261. QImage::Format_ARGB32_Premultiplied);
  262. }
  263. }
  264. }
  265. const auto full = QSize(size, size) * ratio;
  266. auto image = (data.image.size() == full)
  267. ? base::take(data.image)
  268. : QImage(full, QImage::Format_ARGB32_Premultiplied);
  269. image.fill(Qt::transparent);
  270. image.setDevicePixelRatio(ratio);
  271. const auto bg = EmptyUserpic::UserpicColor(
  272. EmptyUserpic::ColorIndex(UniqueRowIdFromString(icon)));
  273. auto p = QPainter(&image);
  274. auto hq = PainterHighQualityEnabler(p);
  275. {
  276. auto gradient = QLinearGradient(0, 0, 0, size);
  277. gradient.setStops({
  278. { 0., bg.color1->c },
  279. { 1., bg.color2->c }
  280. });
  281. p.setBrush(gradient);
  282. }
  283. p.setPen(Qt::NoPen);
  284. p.drawEllipse(QRect(0, 0, size, size));
  285. if (!data.icon.isNull()) {
  286. p.drawImage(
  287. QRect(skip, skip, inner, inner),
  288. style::colorizeImage(data.icon, st::historyPeerUserpicFg));
  289. }
  290. p.end();
  291. data.paletteVersion = version;
  292. data.iconLoaded = loaded;
  293. data.image = std::move(image);
  294. }
  295. p.drawImage(x, y, data.image);
  296. }
  297. [[nodiscard]] QByteArray DefaultCenter(Core::GeoLocation initial) {
  298. const auto &use = initial.exact() ? initial : LastExactLocation;
  299. if (!use) {
  300. return "null";
  301. }
  302. return "["_q
  303. + QByteArray::number(use.point.x())
  304. + ","_q
  305. + QByteArray::number(use.point.y())
  306. + "]"_q;
  307. }
  308. [[nodiscard]] QByteArray DefaultBounds() {
  309. const auto country = Core::ResolveCurrentCountryLocation();
  310. if (!country) {
  311. return "null";
  312. }
  313. return "[["_q
  314. + QByteArray::number(country.bounds.x())
  315. + ","_q
  316. + QByteArray::number(country.bounds.y())
  317. + "],["_q
  318. + QByteArray::number(country.bounds.x() + country.bounds.width())
  319. + ","_q
  320. + QByteArray::number(country.bounds.y() + country.bounds.height())
  321. + "]]"_q;
  322. }
  323. [[nodiscard]] QByteArray ComputeStyles() {
  324. static const auto map = base::flat_map<QByteArray, const style::color*>{
  325. { "window-bg", &st::windowBg },
  326. { "window-bg-over", &st::windowBgOver },
  327. { "window-bg-ripple", &st::windowBgRipple },
  328. { "window-active-text-fg", &st::windowActiveTextFg },
  329. { "history-to-down-shadow", &st::historyToDownShadow },
  330. };
  331. static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
  332. { "maps-places-in-area", tr::lng_maps_places_in_area },
  333. };
  334. return Ui::ComputeStyles(map, phrases, 100, Window::Theme::IsNightMode());
  335. }
  336. [[nodiscard]] QByteArray ReadResource(const QString &name) {
  337. auto file = QFile(u":/picker/"_q + name);
  338. return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
  339. }
  340. [[nodiscard]] QByteArray PickerContent() {
  341. return R"(<!DOCTYPE html>
  342. <html style=")"
  343. + EscapeForAttribute(ComputeStyles())
  344. + R"(">
  345. <head>
  346. <meta charset="utf-8">
  347. <meta name="robots" content="noindex, nofollow">
  348. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  349. <script src="/location/picker.js"></script>
  350. <link rel="stylesheet" href="/location/picker.css" />
  351. <script src='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.js'></script>
  352. <link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
  353. </head>
  354. <body>
  355. <div id="search_venues">
  356. <div id="search_venues_inner"><span id="search_venues_content"></span></div>
  357. </div>
  358. <div id="marker">
  359. <div id="marker_shadow" style="transform: translate(0px, -14px);">
  360. <svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
  361. <defs>
  362. <radialGradient id="shadowGradient">
  363. <stop offset="10%" stop-opacity="0.4"></stop>
  364. <stop offset="100%" stop-opacity="0.05"></stop>
  365. </radialGradient>
  366. </defs>
  367. <ellipse
  368. cx="13.5"
  369. cy="34.8"
  370. rx="10.5"
  371. ry="5.25"
  372. fill=")" + "url(#shadowGradient)" + R"("></ellipse>
  373. </svg>
  374. </div>
  375. <div id="marker_drop" style="transform: translate(0px, -14px);">
  376. <svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
  377. <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>
  378. <circle fill="white" cx="13.5" cy="13.5" r="5.5"></circle>
  379. </svg>
  380. </div>
  381. </div>
  382. <div id="map"></div>
  383. <script>LocationPicker.notify({ event: 'ready' });</script>
  384. </body>
  385. </html>
  386. )"_q;
  387. }
  388. [[nodiscard]] object_ptr<AbstractButton> MakeChooseLocationButton(
  389. QWidget *parent,
  390. rpl::producer<QString> label,
  391. rpl::producer<QString> address) {
  392. auto result = object_ptr<FlatButton>(
  393. parent,
  394. QString(),
  395. st::pickLocationButton);
  396. const auto raw = result.data();
  397. const auto st = &st::pickLocationVenueItem;
  398. const auto icon = CreateChild<RpWidget>(raw);
  399. icon->setGeometry(
  400. st->photoPosition.x(),
  401. st->photoPosition.y(),
  402. st->photoSize,
  403. st->photoSize);
  404. icon->paintRequest() | rpl::start_with_next([=] {
  405. auto p = QPainter(icon);
  406. auto hq = PainterHighQualityEnabler(p);
  407. p.setPen(Qt::NoPen);
  408. p.setBrush(st::windowBgActive);
  409. p.drawEllipse(icon->rect());
  410. st::pickLocationSendIcon.paintInCenter(p, icon->rect());
  411. }, icon->lifetime());
  412. icon->show();
  413. const auto hadAddress = std::make_shared<bool>(false);
  414. auto statusText = std::move(
  415. address
  416. ) | rpl::map([=](const QString &text) {
  417. if (!text.isEmpty()) {
  418. *hadAddress = true;
  419. return text;
  420. }
  421. return *hadAddress ? tr::lng_contacts_loading(tr::now) : QString();
  422. });
  423. const auto name = CreateChild<FlatLabel>(
  424. raw,
  425. std::move(label),
  426. st::pickLocationButtonText);
  427. name->show();
  428. const auto status = CreateChild<FlatLabel>(
  429. raw,
  430. rpl::duplicate(statusText),
  431. st::pickLocationButtonStatus);
  432. status->showOn(rpl::duplicate(
  433. statusText
  434. ) | rpl::map([](const QString &text) {
  435. return !text.isEmpty();
  436. }) | rpl::distinct_until_changed());
  437. rpl::combine(
  438. result->widthValue(),
  439. std::move(statusText)
  440. ) | rpl::start_with_next([=](int width, const QString &statusText) {
  441. const auto available = width
  442. - st->namePosition.x()
  443. - st->button.padding.right();
  444. const auto namePosition = st->namePosition;
  445. const auto statusPosition = st->statusPosition;
  446. name->resizeToWidth(available);
  447. const auto nameTop = statusText.isEmpty()
  448. ? ((st->height - name->height()) / 2)
  449. : namePosition.y();
  450. name->moveToLeft(namePosition.x(), nameTop, width);
  451. status->resizeToNaturalWidth(available);
  452. status->moveToLeft(statusPosition.x(), statusPosition.y(), width);
  453. }, name->lifetime());
  454. icon->setAttribute(Qt::WA_TransparentForMouseEvents);
  455. name->setAttribute(Qt::WA_TransparentForMouseEvents);
  456. status->setAttribute(Qt::WA_TransparentForMouseEvents);
  457. return result;
  458. }
  459. void SetupLoadingView(not_null<RpWidget*> container) {
  460. class Loading final : public RpWidget {
  461. public:
  462. explicit Loading(QWidget *parent)
  463. : RpWidget(parent)
  464. , animation(
  465. [=] { if (!anim::Disabled()) update(); },
  466. st::pickLocationLoading) {
  467. animation.start(st::pickLocationLoading.sineDuration);
  468. }
  469. private:
  470. void paintEvent(QPaintEvent *e) override {
  471. auto p = QPainter(this);
  472. const auto size = st::pickLocationLoading.size;
  473. const auto inner = QRect(QPoint(), size);
  474. const auto positioned = style::centerrect(rect(), inner);
  475. animation.draw(p, positioned.topLeft(), size, width());
  476. }
  477. InfiniteRadialAnimation animation;
  478. };
  479. const auto view = CreateChild<Loading>(container);
  480. view->resize(container->width(), st::recentPeersEmptyHeightMin);
  481. view->show();
  482. ResizeFitChild(container, view);
  483. }
  484. void SetupEmptyView(
  485. not_null<RpWidget*> container,
  486. std::optional<QString> query) {
  487. using Icon = Dialogs::SearchEmptyIcon;
  488. const auto view = CreateChild<Dialogs::SearchEmpty>(
  489. container,
  490. (query ? Icon::NoResults : Icon::Search),
  491. (query
  492. ? tr::lng_maps_no_places
  493. : tr::lng_maps_choose_to_search)(Text::WithEntities));
  494. view->setMinimalHeight(st::recentPeersEmptyHeightMin);
  495. view->show();
  496. ResizeFitChild(container, view);
  497. InvokeQueued(view, [=] { view->animate(); });
  498. }
  499. void SetupVenues(
  500. not_null<VerticalLayout*> container,
  501. std::shared_ptr<Main::SessionShow> show,
  502. rpl::producer<PickerVenueState> value,
  503. Fn<void(VenueData)> callback) {
  504. const auto otherWrap = container->add(object_ptr<SlideWrap<RpWidget>>(
  505. container,
  506. object_ptr<RpWidget>(container)));
  507. const auto other = otherWrap->entity();
  508. rpl::duplicate(
  509. value
  510. ) | rpl::start_with_next([=](const PickerVenueState &state) {
  511. while (!other->children().isEmpty()) {
  512. delete other->children()[0];
  513. }
  514. if (v::is<PickerVenueList>(state)) {
  515. otherWrap->hide(anim::type::instant);
  516. return;
  517. } else if (v::is<PickerVenueLoading>(state)) {
  518. SetupLoadingView(other);
  519. } else {
  520. const auto n = std::get_if<PickerVenueNothingFound>(&state);
  521. SetupEmptyView(other, n ? n->query : std::optional<QString>());
  522. }
  523. otherWrap->show(anim::type::instant);
  524. }, otherWrap->lifetime());
  525. auto &lifetime = container->lifetime();
  526. auto venuesList = rpl::duplicate(
  527. value
  528. ) | rpl::map([=](PickerVenueState &&state) {
  529. return v::is<PickerVenueList>(state)
  530. ? std::move(v::get<PickerVenueList>(state).list)
  531. : std::vector<VenueData>();
  532. });
  533. const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
  534. show);
  535. const auto controller = lifetime.make_state<VenuesController>(
  536. &show->session(),
  537. std::move(venuesList),
  538. std::move(callback));
  539. controller->setStyleOverrides(&st::pickLocationVenueList);
  540. const auto content = container->add(object_ptr<PeerListContent>(
  541. container,
  542. controller));
  543. delegate->setContent(content);
  544. controller->setDelegate(delegate);
  545. show->session().downloaderTaskFinished() | rpl::start_with_next([=] {
  546. content->update();
  547. }, content->lifetime());
  548. }
  549. [[nodiscard]] PickerVenueList ParseVenues(
  550. not_null<Main::Session*> session,
  551. const MTPmessages_BotResults &venues) {
  552. const auto &data = venues.data();
  553. session->data().processUsers(data.vusers());
  554. auto &list = data.vresults().v;
  555. auto result = PickerVenueList();
  556. result.list.reserve(list.size());
  557. for (const auto &found : list) {
  558. found.match([&](const auto &data) {
  559. data.vsend_message().match([&](
  560. const MTPDbotInlineMessageMediaVenue &data) {
  561. data.vgeo().match([&](const MTPDgeoPoint &geo) {
  562. result.list.push_back({
  563. .lat = geo.vlat().v,
  564. .lon = geo.vlong().v,
  565. .title = qs(data.vtitle()),
  566. .address = qs(data.vaddress()),
  567. .provider = qs(data.vprovider()),
  568. .id = qs(data.vvenue_id()),
  569. .venueType = qs(data.vvenue_type()),
  570. });
  571. }, [](const auto &) {});
  572. }, [](const auto &) {});
  573. });
  574. }
  575. return result;
  576. }
  577. not_null<RpWidget*> SetupMapPlaceholder(
  578. not_null<RpWidget*> parent,
  579. int minHeight,
  580. int maxHeight,
  581. Fn<void()> choose) {
  582. const auto result = CreateChild<RpWidget>(parent);
  583. const auto top = CreateChild<BoxContentDivider>(result);
  584. const auto bottom = CreateChild<BoxContentDivider>(result);
  585. const auto icon = CreateChild<RpWidget>(result);
  586. const auto iconSize = st::settingsCloudPasswordIconSize;
  587. auto ownedLottie = Lottie::MakeIcon({
  588. .name = u"location"_q,
  589. .sizeOverride = { iconSize, iconSize },
  590. .limitFps = true,
  591. });
  592. const auto lottie = ownedLottie.get();
  593. icon->lifetime().add([kept = std::move(ownedLottie)] {});
  594. icon->paintRequest(
  595. ) | rpl::start_with_next([=] {
  596. auto p = QPainter(icon);
  597. const auto left = (icon->width() - iconSize) / 2;
  598. const auto scale = icon->height() / float64(iconSize);
  599. auto hq = std::optional<PainterHighQualityEnabler>();
  600. if (scale < 1.) {
  601. const auto center = QPointF(
  602. icon->width() / 2.,
  603. icon->height() / 2.);
  604. hq.emplace(p);
  605. p.translate(center);
  606. p.scale(scale, scale);
  607. p.translate(-center);
  608. p.setOpacity(scale);
  609. }
  610. lottie->paint(p, left, 0);
  611. }, icon->lifetime());
  612. InvokeQueued(icon, [=] {
  613. const auto till = lottie->framesCount() - 1;
  614. lottie->animate([=] { icon->update(); }, 0, till);
  615. });
  616. const auto button = CreateChild<RoundButton>(
  617. result,
  618. tr::lng_maps_select_on_map(),
  619. st::pickLocationChooseOnMap);
  620. button->setFullRadius(true);
  621. button->setTextTransform(RoundButton::TextTransform::NoTransform);
  622. button->setClickedCallback(choose);
  623. parent->sizeValue() | rpl::start_with_next([=](QSize size) {
  624. result->setGeometry(QRect(QPoint(), size));
  625. const auto width = size.width();
  626. top->setGeometry(0, 0, width, top->height());
  627. bottom->setGeometry(QRect(
  628. QPoint(0, size.height() - bottom->height()),
  629. QSize(width, bottom->height())));
  630. const auto dividers = top->height() + bottom->height();
  631. const auto ratio = (size.height() - minHeight)
  632. / float64(maxHeight - minHeight);
  633. const auto iconHeight = int(base::SafeRound(ratio * iconSize));
  634. const auto available = size.height() - dividers;
  635. const auto maxDelta = (maxHeight
  636. - dividers
  637. - iconSize
  638. - button->height()) / 2;
  639. const auto minDelta = (minHeight - dividers - button->height()) / 2;
  640. const auto delta = anim::interpolate(minDelta, maxDelta, ratio);
  641. button->move(
  642. (width - button->width()) / 2,
  643. size.height() - bottom->height() - delta - button->height());
  644. const auto wide = available - delta - button->height();
  645. const auto skip = (wide - iconHeight) / 2;
  646. icon->setGeometry(0, top->height() + skip, width, iconHeight);
  647. }, result->lifetime());
  648. top->show();
  649. icon->show();
  650. bottom->show();
  651. result->show();
  652. return result;
  653. }
  654. } // namespace
  655. LocationPicker::LocationPicker(Descriptor &&descriptor)
  656. : _config(std::move(descriptor.config))
  657. , _callback(std::move(descriptor.callback))
  658. , _quit(std::move(descriptor.quit))
  659. , _window(std::make_unique<SeparatePanel>())
  660. , _body((_window->setInnerSize(st::pickLocationWindow)
  661. , _window->showInner(base::make_unique_q<RpWidget>(_window.get()))
  662. , _window->inner()))
  663. , _chooseButtonLabel(std::move(descriptor.chooseLabel))
  664. , _webviewStorageId(descriptor.storageId)
  665. , _updateStyles([=] {
  666. const auto str = EscapeForScriptString(ComputeStyles());
  667. if (_webview) {
  668. _webview->eval("LocationPicker.updateStyles('" + str + "');");
  669. }
  670. })
  671. , _geocoderResolveTimer([=] { resolveAddressByTimer(); })
  672. , _venueState(PickerVenueLoading())
  673. , _session(descriptor.session)
  674. , _venuesSearchDebounceTimer([=] {
  675. Expects(_venuesSearchLocation.has_value());
  676. Expects(_venuesSearchQuery.has_value());
  677. venuesRequest(*_venuesSearchLocation, *_venuesSearchQuery);
  678. })
  679. , _api(&_session->mtp())
  680. , _venueRecipient(descriptor.recipient) {
  681. std::move(
  682. descriptor.closeRequests
  683. ) | rpl::start_with_next([=] {
  684. _window = nullptr;
  685. delete this;
  686. }, _lifetime);
  687. setup(descriptor);
  688. }
  689. std::shared_ptr<Main::SessionShow> LocationPicker::uiShow() {
  690. return Main::MakeSessionShow(nullptr, _session);
  691. }
  692. bool LocationPicker::Available(const LocationPickerConfig &config) {
  693. static const auto Supported = [&] {
  694. const auto availability = Webview::Availability();
  695. return availability.customSchemeRequests
  696. && availability.customReferer;
  697. }();
  698. return Supported && !config.mapsToken.isEmpty();
  699. }
  700. void LocationPicker::setup(const Descriptor &descriptor) {
  701. setupWindow(descriptor);
  702. _initialProvided = descriptor.initial;
  703. const auto initial = _initialProvided.exact()
  704. ? _initialProvided
  705. : LastExactLocation;
  706. if (initial) {
  707. venuesRequest(initial);
  708. resolveAddress(initial);
  709. venuesSearchEnableAt(initial);
  710. }
  711. if (!_initialProvided) {
  712. resolveCurrentLocation();
  713. }
  714. }
  715. void LocationPicker::setupWindow(const Descriptor &descriptor) {
  716. const auto window = _window.get();
  717. window->setWindowFlag(Qt::WindowStaysOnTopHint, false);
  718. window->closeRequests() | rpl::start_with_next([=] {
  719. close();
  720. }, _lifetime);
  721. const auto parent = descriptor.parent
  722. ? descriptor.parent->window()->geometry()
  723. : QGuiApplication::primaryScreen()->availableGeometry();
  724. window->setTitle(tr::lng_maps_point());
  725. window->move(
  726. parent.x() + (parent.width() - window->width()) / 2,
  727. parent.y() + (parent.height() - window->height()) / 2);
  728. _container = CreateChild<RpWidget>(_body.get());
  729. _mapPlaceholderAdded = st::pickLocationButtonSkip
  730. + st::pickLocationButton.height
  731. + st::pickLocationButtonSkip
  732. + st::boxDividerHeight;
  733. const auto min = st::pickLocationCollapsedHeight + _mapPlaceholderAdded;
  734. const auto max = st::pickLocationMapHeight + _mapPlaceholderAdded;
  735. _mapPlaceholder = SetupMapPlaceholder(_container, min, max, [=] {
  736. setupWebview();
  737. });
  738. _scroll = CreateChild<ScrollArea>(_body.get());
  739. const auto controls = _scroll->setOwnedWidget(
  740. object_ptr<VerticalLayout>(_scroll));
  741. _mapControlsWrap = controls->add(
  742. object_ptr<SlideWrap<VerticalLayout>>(
  743. controls,
  744. object_ptr<VerticalLayout>(controls)));
  745. _mapControlsWrap->show(anim::type::instant);
  746. const auto mapControls = _mapControlsWrap->entity();
  747. const auto toppad = mapControls->add(object_ptr<RpWidget>(controls));
  748. AddSkip(mapControls);
  749. AddSubsectionTitle(mapControls, tr::lng_maps_or_choose());
  750. auto state = _venueState.value();
  751. SetupVenues(controls, uiShow(), std::move(state), [=](VenueData info) {
  752. _callback(std::move(info));
  753. close();
  754. });
  755. rpl::combine(
  756. _body->sizeValue(),
  757. _scroll->scrollTopValue(),
  758. _venuesSearchShown.value()
  759. ) | rpl::start_with_next([=](QSize size, int scrollTop, bool search) {
  760. const auto width = size.width();
  761. const auto height = size.height();
  762. const auto sub = std::min(
  763. (st::pickLocationMapHeight - st::pickLocationCollapsedHeight),
  764. scrollTop);
  765. const auto mapHeight = st::pickLocationMapHeight
  766. - sub
  767. + (_mapPlaceholder ? _mapPlaceholderAdded : 0);
  768. _container->setGeometry(0, 0, width, mapHeight);
  769. const auto scrollWidgetTop = search ? 0 : mapHeight;
  770. const auto scrollHeight = height - scrollWidgetTop;
  771. _scroll->setGeometry(0, scrollWidgetTop, width, scrollHeight);
  772. controls->resizeToWidth(width);
  773. toppad->resize(width, sub);
  774. }, _container->lifetime());
  775. _container->paintRequest() | rpl::start_with_next([=](QRect clip) {
  776. QPainter(_container).fillRect(clip, st::windowBg);
  777. }, _container->lifetime());
  778. _container->show();
  779. _scroll->show();
  780. controls->show();
  781. window->show();
  782. }
  783. void LocationPicker::setupWebview() {
  784. Expects(!_webview);
  785. delete base::take(_mapPlaceholder);
  786. const auto mapControls = _mapControlsWrap->entity();
  787. mapControls->insert(
  788. 1,
  789. object_ptr<BoxContentDivider>(mapControls)
  790. )->show();
  791. _mapButton = mapControls->insert(
  792. 1,
  793. MakeChooseLocationButton(
  794. mapControls,
  795. _chooseButtonLabel.value(),
  796. _geocoderAddress.value()),
  797. { 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });
  798. _mapButton->setClickedCallback([=] {
  799. _webview->eval("LocationPicker.send();");
  800. });
  801. _mapButton->hide();
  802. _scroll->scrollToY(0);
  803. _venuesSearchShown.force_assign(_venuesSearchShown.current());
  804. _mapLoading = CreateChild<RpWidget>(_body.get());
  805. _container->geometryValue() | rpl::start_with_next([=](QRect rect) {
  806. _mapLoading->setGeometry(rect);
  807. }, _mapLoading->lifetime());
  808. SetupLoadingView(_mapLoading);
  809. _mapLoading->show();
  810. const auto window = _window.get();
  811. _webview = std::make_unique<Webview::Window>(
  812. _container,
  813. Webview::WindowConfig{
  814. .opaqueBg = st::windowBg->c,
  815. .storageId = _webviewStorageId,
  816. .dataProtocolOverride = kProtocolOverride,
  817. });
  818. const auto raw = _webview.get();
  819. window->lifetime().add([=] {
  820. _webview = nullptr;
  821. });
  822. window->events(
  823. ) | rpl::start_with_next([=](not_null<QEvent*> e) {
  824. if (e->type() == QEvent::Close) {
  825. close();
  826. } else if (e->type() == QEvent::KeyPress) {
  827. const auto event = static_cast<QKeyEvent*>(e.get());
  828. if (event->key() == Qt::Key_Escape && !_venuesSearchQuery) {
  829. close();
  830. }
  831. }
  832. }, window->lifetime());
  833. raw->widget()->show();
  834. _container->sizeValue(
  835. ) | rpl::start_with_next([=](QSize size) {
  836. raw->widget()->setGeometry(QRect(QPoint(), size));
  837. }, _container->lifetime());
  838. raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
  839. return true;
  840. });
  841. raw->setNavigationDoneHandler([=](bool success) {
  842. });
  843. raw->setMessageHandler([=](const QJsonDocument &message) {
  844. crl::on_main(_window.get(), [=] {
  845. const auto object = message.object();
  846. const auto event = object.value("event").toString();
  847. if (event == u"ready"_q) {
  848. mapReady();
  849. } else if (event == u"keydown"_q) {
  850. const auto key = object.value("key").toString();
  851. const auto modifier = object.value("modifier").toString();
  852. processKey(key, modifier);
  853. } else if (event == u"send"_q) {
  854. const auto lat = object.value("latitude").toDouble();
  855. const auto lon = object.value("longitude").toDouble();
  856. _callback({
  857. .lat = lat,
  858. .lon = lon,
  859. .address = _geocoderAddress.current(),
  860. });
  861. close();
  862. } else if (event == u"move_start"_q) {
  863. if (const auto now = _geocoderAddress.current()
  864. ; !now.isEmpty()) {
  865. _geocoderSavedAddress = now;
  866. _geocoderAddress = QString();
  867. }
  868. base::take(_geocoderResolvePostponed);
  869. _geocoderResolveTimer.cancel();
  870. } else if (event == u"move_end"_q) {
  871. const auto lat = object.value("latitude").toDouble();
  872. const auto lon = object.value("longitude").toDouble();
  873. const auto location = Core::GeoLocation{
  874. .point = { lat, lon },
  875. .accuracy = Core::GeoLocationAccuracy::Exact,
  876. };
  877. if (AreTheSame(_geocoderResolvingFor, location)
  878. && !_geocoderSavedAddress.isEmpty()) {
  879. _geocoderAddress = base::take(_geocoderSavedAddress);
  880. _geocoderResolveTimer.cancel();
  881. } else {
  882. _geocoderResolvePostponed = location;
  883. _geocoderResolveTimer.callOnce(kResolveAddressDelay);
  884. }
  885. if (!AreTheSame(_venuesRequestLocation, location)) {
  886. _webview->eval(
  887. "LocationPicker.toggleSearchVenues(true);");
  888. }
  889. venuesSearchEnableAt(location);
  890. } else if (event == u"search_venues"_q) {
  891. const auto lat = object.value("latitude").toDouble();
  892. const auto lon = object.value("longitude").toDouble();
  893. venuesRequest({
  894. .point = { lat, lon },
  895. .accuracy = Core::GeoLocationAccuracy::Exact,
  896. });
  897. }
  898. });
  899. });
  900. raw->setDataRequestHandler([=](Webview::DataRequest request) {
  901. const auto pos = request.id.find('#');
  902. if (pos != request.id.npos) {
  903. request.id = request.id.substr(0, pos);
  904. }
  905. if (!request.id.starts_with("location/")) {
  906. return Webview::DataResult::Failed;
  907. }
  908. const auto finishWith = [&](QByteArray data, std::string mime) {
  909. request.done({
  910. .stream = std::make_unique<Webview::DataStreamFromMemory>(
  911. std::move(data),
  912. std::move(mime)),
  913. });
  914. return Webview::DataResult::Done;
  915. };
  916. if (!_subscribedToColors) {
  917. _subscribedToColors = true;
  918. rpl::merge(
  919. Lang::Updated(),
  920. style::PaletteChanged()
  921. ) | rpl::start_with_next([=] {
  922. _updateStyles.call();
  923. }, _webview->lifetime());
  924. }
  925. const auto id = std::string_view(request.id).substr(9);
  926. if (id == "picker.html") {
  927. return finishWith(PickerContent(), "text/html; charset=utf-8");
  928. }
  929. const auto css = id.ends_with(".css");
  930. const auto js = !css && id.ends_with(".js");
  931. if (!css && !js) {
  932. return Webview::DataResult::Failed;
  933. }
  934. const auto qstring = QString::fromUtf8(id.data(), id.size());
  935. const auto pattern = u"^[a-zA-Z\\.\\-_0-9]+$"_q;
  936. if (QRegularExpression(pattern).match(qstring).hasMatch()) {
  937. const auto bytes = ReadResource(qstring);
  938. if (!bytes.isEmpty()) {
  939. const auto mime = css ? "text/css" : "text/javascript";
  940. return finishWith(bytes, mime);
  941. }
  942. }
  943. return Webview::DataResult::Failed;
  944. });
  945. raw->init(R"()");
  946. raw->navigateToData("location/picker.html");
  947. }
  948. void LocationPicker::resolveAddressByTimer() {
  949. if (const auto location = base::take(_geocoderResolvePostponed)) {
  950. resolveAddress(location);
  951. }
  952. }
  953. void LocationPicker::resolveAddress(Core::GeoLocation location) {
  954. if (AreTheSame(_geocoderResolvingFor, location)) {
  955. return;
  956. }
  957. _geocoderResolvingFor = location;
  958. const auto done = [=](Core::GeoAddress address) {
  959. if (!AreTheSame(_geocoderResolvingFor, location)) {
  960. return;
  961. } else if (address) {
  962. _geocoderAddress = address.name;
  963. } else {
  964. _geocoderAddress = u"(%1, %2)"_q
  965. .arg(location.point.x(), 0, 'f')
  966. .arg(location.point.y(), 0, 'f');
  967. }
  968. };
  969. const auto baseLangId = Lang::GetInstance().baseId();
  970. const auto langId = baseLangId.isEmpty()
  971. ? Lang::GetInstance().id()
  972. : baseLangId;
  973. const auto nonEmptyId = langId.isEmpty() ? u"en"_q : langId;
  974. Core::ResolveLocationAddress(
  975. location,
  976. langId,
  977. _config.geoToken,
  978. crl::guard(this, done));
  979. }
  980. void LocationPicker::mapReady() {
  981. Expects(_scroll != nullptr);
  982. delete base::take(_mapLoading);
  983. const auto token = _config.mapsToken.toUtf8();
  984. const auto center = DefaultCenter(_initialProvided);
  985. const auto bounds = DefaultBounds();
  986. const auto protocol = *kProtocolOverride
  987. ? "'"_q + kProtocolOverride + "'"
  988. : "null";
  989. const auto params = "token: '" + token + "'"
  990. + ", center: " + center
  991. + ", bounds: " + bounds
  992. + ", protocol: " + protocol;
  993. _webview->eval("LocationPicker.init({ " + params + " });");
  994. const auto handle = _window->window()->windowHandle();
  995. if (handle && QGuiApplication::focusWindow() == handle) {
  996. _webview->focus();
  997. }
  998. _mapButton->show();
  999. }
  1000. bool LocationPicker::venuesFromCache(
  1001. Core::GeoLocation location,
  1002. QString query) {
  1003. const auto normalized = NormalizeVenuesQuery(query);
  1004. auto &cache = _venuesCache[normalized];
  1005. const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {
  1006. return AreTheSame(v.location, location);
  1007. });
  1008. if (i == end(cache)) {
  1009. return false;
  1010. }
  1011. _venuesRequestLocation = location;
  1012. _venuesRequestQuery = normalized;
  1013. _venuesInitialQuery = query;
  1014. venuesApplyResults(i->result);
  1015. return true;
  1016. }
  1017. void LocationPicker::venuesRequest(
  1018. Core::GeoLocation location,
  1019. QString query) {
  1020. const auto normalized = NormalizeVenuesQuery(query);
  1021. if (AreTheSame(_venuesRequestLocation, location)
  1022. && _venuesRequestQuery == normalized) {
  1023. return;
  1024. } else if (const auto oldRequestId = base::take(_venuesRequestId)) {
  1025. _api.request(oldRequestId).cancel();
  1026. }
  1027. _venueState = PickerVenueLoading();
  1028. _venuesRequestLocation = location;
  1029. _venuesRequestQuery = normalized;
  1030. _venuesInitialQuery = query;
  1031. if (_venuesBot) {
  1032. venuesSendRequest();
  1033. } else if (_venuesBotRequestId) {
  1034. return;
  1035. }
  1036. const auto username = _session->serverConfig().venueSearchUsername;
  1037. _venuesBotRequestId = _api.request(MTPcontacts_ResolveUsername(
  1038. MTP_flags(0),
  1039. MTP_string(username),
  1040. MTP_string()
  1041. )).done([=](const MTPcontacts_ResolvedPeer &result) {
  1042. auto &data = result.data();
  1043. _session->data().processUsers(data.vusers());
  1044. _session->data().processChats(data.vchats());
  1045. const auto peer = _session->data().peerLoaded(
  1046. peerFromMTP(data.vpeer()));
  1047. const auto user = peer ? peer->asUser() : nullptr;
  1048. if (user && user->isBotInlineGeo()) {
  1049. _venuesBot = user;
  1050. venuesSendRequest();
  1051. } else {
  1052. LOG(("API Error: Bad peer returned by: %1").arg(username));
  1053. }
  1054. }).fail([=] {
  1055. LOG(("API Error: Error returned on lookup: %1").arg(username));
  1056. }).send();
  1057. }
  1058. void LocationPicker::venuesSendRequest() {
  1059. Expects(_venuesBot != nullptr);
  1060. if (_venuesRequestId || !_venuesRequestLocation) {
  1061. return;
  1062. }
  1063. _venuesRequestId = _api.request(MTPmessages_GetInlineBotResults(
  1064. MTP_flags(MTPmessages_GetInlineBotResults::Flag::f_geo_point),
  1065. _venuesBot->inputUser,
  1066. (_venueRecipient ? _venueRecipient->input : MTP_inputPeerEmpty()),
  1067. MTP_inputGeoPoint(
  1068. MTP_flags(0),
  1069. MTP_double(_venuesRequestLocation.point.x()),
  1070. MTP_double(_venuesRequestLocation.point.y()),
  1071. MTP_int(0)), // accuracy_radius
  1072. MTP_string(_venuesRequestQuery),
  1073. MTP_string() // offset
  1074. )).done([=](const MTPmessages_BotResults &result) {
  1075. auto parsed = ParseVenues(_session, result);
  1076. _venuesCache[_venuesRequestQuery].push_back({
  1077. .location = _venuesRequestLocation,
  1078. .result = parsed,
  1079. });
  1080. venuesApplyResults(std::move(parsed));
  1081. }).fail([=] {
  1082. venuesApplyResults({});
  1083. }).send();
  1084. }
  1085. void LocationPicker::venuesApplyResults(PickerVenueList venues) {
  1086. _venuesRequestId = 0;
  1087. if (venues.list.empty()) {
  1088. _venueState = PickerVenueNothingFound{ _venuesInitialQuery };
  1089. } else {
  1090. _venueState = std::move(venues);
  1091. }
  1092. }
  1093. void LocationPicker::venuesSearchEnableAt(Core::GeoLocation location) {
  1094. if (!_venuesSearchLocation) {
  1095. _window->setSearchAllowed(
  1096. tr::lng_dlg_filter(),
  1097. [=](std::optional<QString> query) {
  1098. venuesSearchChanged(query);
  1099. });
  1100. }
  1101. _venuesSearchLocation = location;
  1102. }
  1103. void LocationPicker::venuesSearchChanged(
  1104. const std::optional<QString> &query) {
  1105. _venuesSearchQuery = query;
  1106. const auto shown = query && !query->trimmed().isEmpty();
  1107. _venuesSearchShown = shown;
  1108. if (_container->isHidden() != shown) {
  1109. _container->setVisible(!shown);
  1110. _mapControlsWrap->toggle(!shown, anim::type::instant);
  1111. if (shown) {
  1112. _venuesNoSearchLocation = _venuesRequestLocation;
  1113. } else if (_venuesNoSearchLocation) {
  1114. if (!venuesFromCache(_venuesNoSearchLocation)) {
  1115. venuesRequest(_venuesNoSearchLocation);
  1116. }
  1117. }
  1118. }
  1119. if (shown
  1120. && !venuesFromCache(
  1121. *_venuesSearchLocation,
  1122. *_venuesSearchQuery)) {
  1123. _venueState = PickerVenueLoading();
  1124. _venuesSearchDebounceTimer.callOnce(kSearchDebounceDelay);
  1125. } else {
  1126. _venuesSearchDebounceTimer.cancel();
  1127. }
  1128. }
  1129. void LocationPicker::resolveCurrentLocation() {
  1130. using namespace Core;
  1131. const auto window = _window.get();
  1132. ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
  1133. const auto changed = !AreTheSame(LastExactLocation, location);
  1134. if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
  1135. if (!_venuesSearchLocation) {
  1136. _venueState = PickerVenueWaitingForLocation();
  1137. }
  1138. return;
  1139. }
  1140. LastExactLocation = location;
  1141. if (location) {
  1142. if (_venuesSearchQuery.value_or(QString()).isEmpty()) {
  1143. venuesRequest(location);
  1144. }
  1145. resolveAddress(location);
  1146. }
  1147. if (_webview) {
  1148. const auto point = QByteArray::number(location.point.x())
  1149. + ","_q
  1150. + QByteArray::number(location.point.y());
  1151. _webview->eval("LocationPicker.narrowTo([" + point + "]);");
  1152. }
  1153. }));
  1154. }
  1155. void LocationPicker::processKey(
  1156. const QString &key,
  1157. const QString &modifier) {
  1158. const auto ctrl = ::Platform::IsMac() ? u"cmd"_q : u"ctrl"_q;
  1159. if (key == u"escape"_q) {
  1160. if (!_window->closeSearch()) {
  1161. close();
  1162. }
  1163. } else if (key == u"w"_q && modifier == ctrl) {
  1164. close();
  1165. } else if (key == u"m"_q && modifier == ctrl) {
  1166. minimize();
  1167. } else if (key == u"q"_q && modifier == ctrl) {
  1168. quit();
  1169. }
  1170. }
  1171. void LocationPicker::activate() {
  1172. if (_window) {
  1173. _window->activateWindow();
  1174. }
  1175. }
  1176. void LocationPicker::close() {
  1177. crl::on_main(this, [=] {
  1178. _window = nullptr;
  1179. delete this;
  1180. });
  1181. }
  1182. void LocationPicker::minimize() {
  1183. if (_window) {
  1184. _window->setWindowState(_window->windowState()
  1185. | Qt::WindowMinimized);
  1186. }
  1187. }
  1188. void LocationPicker::quit() {
  1189. if (const auto onstack = _quit) {
  1190. onstack();
  1191. }
  1192. }
  1193. not_null<LocationPicker*> LocationPicker::Show(Descriptor &&descriptor) {
  1194. return new LocationPicker(std::move(descriptor));
  1195. }
  1196. } // namespace Ui