diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 48cf60eff..b9ee4785c 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -475,6 +475,8 @@ PRIVATE data/business/data_shortcut_messages.h data/components/factchecks.cpp data/components/factchecks.h + data/components/location_pickers.cpp + data/components/location_pickers.h data/components/recent_peers.cpp data/components/recent_peers.h data/components/scheduled_messages.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6c9828189..b902aa4f6 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3197,6 +3197,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_unread_bar_some" = "Unread messages"; "lng_maps_point" = "Location"; +"lng_maps_select_on_map" = "Select on the Map"; "lng_maps_point_send" = "Send This Location"; "lng_maps_point_set" = "Set This Location"; "lng_maps_or_choose" = "Or choose a venue"; diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index cd8aa54e2..81f098d67 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -30,6 +30,10 @@ struct SendOptions { bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; + + friend inline bool operator==( + const SendOptions &, + const SendOptions &) = default; }; [[nodiscard]] SendOptions DefaultSendWhenOnlineOptions(); @@ -52,6 +56,10 @@ struct SendAction { MsgId replaceMediaOf = 0; [[nodiscard]] MTPInputReplyTo mtpReplyTo() const; + + friend inline bool operator==( + const SendAction &, + const SendAction &) = default; }; struct MessageToSend { diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 9b92f4815..47c1e8b47 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1475,3 +1475,9 @@ pickLocationLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { thickness: 4px; } pickLocationPromoHeight: 32px; +pickLocationChooseOnMap: RoundButton(defaultActiveButton) { + height: 44px; + textTop: 11px; + width: -96px; + font: font(15px semibold); +} diff --git a/Telegram/SourceFiles/data/components/location_pickers.cpp b/Telegram/SourceFiles/data/components/location_pickers.cpp new file mode 100644 index 000000000..4402e4ccf --- /dev/null +++ b/Telegram/SourceFiles/data/components/location_pickers.cpp @@ -0,0 +1,44 @@ +/* +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 "data/components/location_pickers.h" + +#include "api/api_common.h" +#include "ui/controls/location_picker.h" + +namespace Data { + +struct LocationPickers::Entry { + Api::SendAction action; + base::weak_ptr picker; +}; + +LocationPickers::LocationPickers() = default; + +LocationPickers::~LocationPickers() = default; + +Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) { + for (auto i = begin(_pickers); i != end(_pickers);) { + if (const auto strong = i->picker.get()) { + if (i->action == action) { + return i->picker.get(); + } + ++i; + } else { + i = _pickers.erase(i); + } + } + return nullptr; +} + +void LocationPickers::emplace( + const Api::SendAction &action, + not_null picker) { + _pickers.push_back({ action, picker }); +} + +} // namespace Data \ No newline at end of file diff --git a/Telegram/SourceFiles/data/components/location_pickers.h b/Telegram/SourceFiles/data/components/location_pickers.h new file mode 100644 index 000000000..ad1046095 --- /dev/null +++ b/Telegram/SourceFiles/data/components/location_pickers.h @@ -0,0 +1,39 @@ +/* +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 +*/ +#pragma once + +#include "base/weak_ptr.h" + +namespace Api { +struct SendAction; +} // namespace Api + +namespace Ui { +class LocationPicker; +} // namespace Ui + +namespace Data { + +class LocationPickers final { +public: + LocationPickers(); + ~LocationPickers(); + + Ui::LocationPicker *lookup(const Api::SendAction &action); + void emplace( + const Api::SendAction &action, + not_null picker); + +private: + struct Entry; + + std::vector _pickers; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 27e37053a..e7d842c64 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "core/click_handler_types.h" #include "core/shortcuts.h" +#include "data/components/location_pickers.h" #include "data/data_bot_app.h" #include "data/data_changes.h" #include "data/data_user.h" @@ -1776,6 +1777,11 @@ void ChooseAndSendLocation( not_null controller, const Ui::LocationPickerConfig &config, Api::SendAction action) { + const auto session = &controller->session(); + if (const auto picker = session->locationPickers().lookup(action)) { + picker->activate(); + return; + } const auto callback = [=](Data::InputVenue venue) { if (venue.justLocation()) { Api::SendLocation(action, venue.lat, venue.lon); @@ -1783,17 +1789,18 @@ void ChooseAndSendLocation( Api::SendVenue(action, venue); } }; - Ui::LocationPicker::Show({ + const auto picker = Ui::LocationPicker::Show({ .parent = controller->widget(), .config = config, .chooseLabel = tr::lng_maps_point_send(), .recipient = action.history->peer, - .session = &controller->session(), - .callback = crl::guard(controller, callback), + .session = session, + .callback = crl::guard(session, callback), .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, - .storageId = controller->session().local().resolveStorageIdBots(), + .storageId = session->local().resolveStorageIdBots(), .closeRequests = controller->content()->death(), }); + session->locationPickers().emplace(action, picker); } std::unique_ptr MakeAttachBotsMenu( diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index c14d49051..cc09dacf5 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "storage/storage_facade.h" #include "data/components/factchecks.h" +#include "data/components/location_pickers.h" #include "data/components/recent_peers.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" @@ -111,6 +112,7 @@ Session::Session( , _sponsoredMessages(std::make_unique(this)) , _topPeers(std::make_unique(this)) , _factchecks(std::make_unique(this)) +, _locationPickers(std::make_unique()) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _saveSettingsTimer([=] { saveSettings(); }) { diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 9581e7cd4..a69373a36 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -36,6 +36,7 @@ class ScheduledMessages; class SponsoredMessages; class TopPeers; class Factchecks; +class LocationPickers; } // namespace Data namespace HistoryView::Reactions { @@ -131,6 +132,9 @@ public: [[nodiscard]] Data::Factchecks &factchecks() const { return *_factchecks; } + [[nodiscard]] Data::LocationPickers &locationPickers() const { + return *_locationPickers; + } [[nodiscard]] Api::Updates &updates() const { return *_updates; } @@ -259,6 +263,7 @@ private: const std::unique_ptr _sponsoredMessages; const std::unique_ptr _topPeers; const std::unique_ptr _factchecks; + const std::unique_ptr _locationPickers; using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; const std::unique_ptr _cachedReactionIconFactory; diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp index e5bb0f8e3..6b659d044 100644 --- a/Telegram/SourceFiles/settings/business/settings_location.cpp +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -63,6 +63,7 @@ private: const Ui::LocationPickerConfig _config; rpl::variable _data; rpl::variable _map = nullptr; + base::weak_ptr _picker; std::shared_ptr _view; Ui::RoundRect _bottomSkipRounding; @@ -232,6 +233,10 @@ void Location::setupPicker(not_null content) { } void Location::chooseOnMap() { + if (const auto strong = _picker.get()) { + strong->activate(); + return; + } const auto callback = [=](Data::InputVenue venue) { auto copy = _data.current(); copy.point = Data::LocationPoint( @@ -249,7 +254,7 @@ void Location::chooseOnMap() { .accuracy = Core::GeoLocationAccuracy::Exact, } : Core::GeoLocation(); - Ui::LocationPicker::Show({ + _picker = Ui::LocationPicker::Show({ .parent = controller()->widget(), .config = _config, .chooseLabel = tr::lng_maps_point_set(), @@ -258,7 +263,7 @@ void Location::chooseOnMap() { .callback = crl::guard(this, callback), .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, .storageId = session->local().resolveStorageIdBots(), - .closeRequests = controller()->content()->death(), + .closeRequests = death(), }); } diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp index 8cce44571..b6e3dc441 100644 --- a/Telegram/SourceFiles/ui/controls/location_picker.cpp +++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" @@ -41,6 +42,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_window.h" +#include "styles/style_settings.h" // settingsCloudPasswordIconSize +#include "styles/style_layers.h" // boxDividerHeight #include #include @@ -185,11 +188,12 @@ private: [[nodiscard]] object_ptr MakeFoursquarePromo() { auto result = object_ptr((QWidget*)nullptr); + const auto skip = st::defaultVerticalListSkip; const auto raw = result.data(); - raw->resize(0, st::pickLocationPromoHeight); + raw->resize(0, skip + st::pickLocationPromoHeight); const auto shadow = CreateChild(raw); raw->widthValue() | rpl::start_with_next([=](int width) { - shadow->setGeometry(0, 0, width, st::lineWidth); + shadow->setGeometry(0, skip, width, st::lineWidth); }, raw->lifetime()); raw->paintRequest() | rpl::start_with_next([=](QRect clip) { auto p = QPainter(raw); @@ -197,7 +201,7 @@ private: p.setPen(st::windowSubTextFg); p.setFont(st::normalFont); p.drawText( - raw->rect(), + raw->rect().marginsRemoved({ 0, skip, 0, 0 }), tr::lng_maps_venues_source(tr::now), style::al_center); }, raw->lifetime()); @@ -544,7 +548,7 @@ void SetupEmptyView( (query ? Icon::NoResults : Icon::Search), (query ? tr::lng_maps_no_places - : tr::lng_maps_choose_to_search)(Ui::Text::WithEntities)); + : tr::lng_maps_choose_to_search)(Text::WithEntities)); view->setMinimalHeight(st::recentPeersEmptyHeightMin); view->show(); @@ -636,6 +640,96 @@ void SetupVenues( return result; } +not_null SetupMapPlaceholder( + not_null parent, + int minHeight, + int maxHeight, + Fn choose) { + const auto result = CreateChild(parent); + + const auto top = CreateChild(result); + const auto bottom = CreateChild(result); + + const auto icon = CreateChild(result); + const auto iconSize = st::settingsCloudPasswordIconSize; + auto ownedLottie = Lottie::MakeIcon({ + .name = u"location"_q, + .sizeOverride = { iconSize, iconSize }, + .limitFps = true, + }); + const auto lottie = ownedLottie.get(); + icon->lifetime().add([kept = std::move(ownedLottie)] {}); + + icon->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(icon); + const auto left = (icon->width() - iconSize) / 2; + const auto scale = icon->height() / float64(iconSize); + auto hq = std::optional(); + if (scale < 1.) { + const auto center = QPointF( + icon->width() / 2., + icon->height() / 2.); + hq.emplace(p); + p.translate(center); + p.scale(scale, scale); + p.translate(-center); + p.setOpacity(scale); + } + lottie->paint(p, left, 0); + }, icon->lifetime()); + + InvokeQueued(icon, [=] { + const auto till = lottie->framesCount() - 1; + lottie->animate([=] { icon->update(); }, 0, till); + }); + + const auto button = CreateChild( + result, + tr::lng_maps_select_on_map(), + st::pickLocationChooseOnMap); + button->setFullRadius(true); + button->setTextTransform(RoundButton::TextTransform::NoTransform); + button->setClickedCallback(choose); + + parent->sizeValue() | rpl::start_with_next([=](QSize size) { + result->setGeometry(QRect(QPoint(), size)); + + const auto width = size.width(); + top->setGeometry(0, 0, width, top->height()); + bottom->setGeometry(QRect( + QPoint(0, size.height() - bottom->height()), + QSize(width, bottom->height()))); + const auto dividers = top->height() + bottom->height(); + + const auto ratio = (size.height() - minHeight) + / float64(maxHeight - minHeight); + const auto iconHeight = int(base::SafeRound(ratio * iconSize)); + + const auto available = size.height() - dividers; + const auto maxDelta = (maxHeight + - dividers + - iconSize + - button->height()) / 2; + const auto minDelta = (minHeight - dividers - button->height()) / 2; + + const auto delta = anim::interpolate(minDelta, maxDelta, ratio); + button->move( + (width - button->width()) / 2, + size.height() - bottom->height() - delta - button->height()); + const auto wide = available - delta - button->height(); + const auto skip = (wide - iconHeight) / 2; + icon->setGeometry(0, top->height() + skip, width, iconHeight); + }, result->lifetime()); + + top->show(); + icon->show(); + bottom->show(); + result->show(); + + return result; +} + } // namespace LocationPicker::LocationPicker(Descriptor &&descriptor) @@ -646,6 +740,8 @@ LocationPicker::LocationPicker(Descriptor &&descriptor) , _body((_window->setInnerSize(st::pickLocationWindow) , _window->showInner(base::make_unique_q(_window.get())) , _window->inner())) +, _chooseButtonLabel(std::move(descriptor.chooseLabel)) +, _webviewStorageId(descriptor.storageId) , _updateStyles([=] { const auto str = EscapeForScriptString(ComputeStyles()); if (_webview) { @@ -684,7 +780,6 @@ bool LocationPicker::Available(const LocationPickerConfig &config) { void LocationPicker::setup(const Descriptor &descriptor) { setupWindow(descriptor); - setupWebview(descriptor); _initialProvided = descriptor.initial; const auto initial = _initialProvided.exact() @@ -695,6 +790,9 @@ void LocationPicker::setup(const Descriptor &descriptor) { resolveAddress(initial); venuesSearchEnableAt(initial); } + if (!_initialProvided) { + resolveCurrentLocation(); + } } void LocationPicker::setupWindow(const Descriptor &descriptor) { @@ -714,6 +812,15 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { parent.y() + (parent.height() - window->height()) / 2); _container = CreateChild(_body.get()); + _mapPlaceholderAdded = st::pickLocationButtonSkip + + st::pickLocationButton.height + + st::pickLocationButtonSkip + + st::boxDividerHeight; + const auto min = st::pickLocationCollapsedHeight + _mapPlaceholderAdded; + const auto max = st::pickLocationMapHeight + _mapPlaceholderAdded; + _mapPlaceholder = SetupMapPlaceholder(_container, min, max, [=] { + setupWebview(); + }); _scroll = CreateChild(_body.get()); const auto controls = _scroll->setOwnedWidget( object_ptr(_scroll)); @@ -727,17 +834,6 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { const auto toppad = mapControls->add(object_ptr(controls)); - const auto button = mapControls->add( - MakeChooseLocationButton( - mapControls, - std::move(descriptor.chooseLabel), - _geocoderAddress.value()), - { 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip }); - button->setClickedCallback([=] { - _webview->eval("LocationPicker.send();"); - }); - - AddDivider(mapControls); AddSkip(mapControls); AddSubsectionTitle(mapControls, tr::lng_maps_or_choose()); @@ -757,7 +853,9 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { const auto sub = std::min( (st::pickLocationMapHeight - st::pickLocationCollapsedHeight), scrollTop); - const auto mapHeight = st::pickLocationMapHeight - sub; + 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; @@ -771,21 +869,52 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { }, _container->lifetime()); _container->show(); - _scroll->hide(); + _scroll->show(); controls->show(); - button->show(); window->show(); } -void LocationPicker::setupWebview(const Descriptor &descriptor) { +void LocationPicker::setupWebview() { Expects(!_webview); + delete base::take(_mapPlaceholder); + + const auto mapControls = _mapControlsWrap->entity(); + mapControls->insert( + 1, + object_ptr(mapControls) + )->show(); + + _mapButton = mapControls->insert( + 1, + MakeChooseLocationButton( + mapControls, + _chooseButtonLabel.value(), + _geocoderAddress.value()), + { 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip }); + _mapButton->setClickedCallback([=] { + _webview->eval("LocationPicker.send();"); + }); + _mapButton->hide(); + + _scroll->scrollToY(0); + _venuesSearchShown.force_assign(_venuesSearchShown.current()); + + _mapLoading = CreateChild(_body.get()); + + _container->geometryValue() | rpl::start_with_next([=](QRect rect) { + _mapLoading->setGeometry(rect); + }, _mapLoading->lifetime()); + + SetupLoadingView(_mapLoading); + _mapLoading->show(); + const auto window = _window.get(); _webview = std::make_unique( _container, Webview::WindowConfig{ .opaqueBg = st::windowBg->c, - .storageId = descriptor.storageId, + .storageId = _webviewStorageId, .dataProtocolOverride = kProtocolOverride, }); const auto raw = _webview.get(); @@ -823,12 +952,6 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) { const auto event = object.value("event").toString(); if (event == u"ready"_q) { mapReady(); - if (!_initialProvided) { - resolveCurrentLocation(); - } - if (_webview) { - _webview->focus(); - } } else if (event == u"keydown"_q) { const auto key = object.value("key").toString(); const auto modifier = object.value("modifier").toString(); @@ -968,6 +1091,8 @@ void LocationPicker::resolveAddress(Core::GeoLocation location) { 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(); @@ -980,7 +1105,11 @@ void LocationPicker::mapReady() { + ", protocol: " + protocol; _webview->eval("LocationPicker.init({ " + params + " });"); - _scroll->show(); + const auto handle = _window->window()->windowHandle(); + if (handle && QGuiApplication::focusWindow() == handle) { + _webview->focus(); + } + _mapButton->show(); } bool LocationPicker::venuesFromCache( @@ -1163,6 +1292,12 @@ void LocationPicker::processKey( } } +void LocationPicker::activate() { + if (_window) { + _window->activateWindow(); + } +} + void LocationPicker::close() { crl::on_main(this, [=] { _window = nullptr; diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h index 97ce724f3..943e7ac75 100644 --- a/Telegram/SourceFiles/ui/controls/location_picker.h +++ b/Telegram/SourceFiles/ui/controls/location_picker.h @@ -29,6 +29,7 @@ class Window; namespace Ui { +class AbstractButton; class SeparatePanel; class RpWidget; class ScrollArea; @@ -93,6 +94,7 @@ public: [[nodiscard]] static bool Available(const LocationPickerConfig &config); static not_null Show(Descriptor &&descriptor); + void activate(); void close(); void minimize(); void quit(); @@ -109,7 +111,7 @@ private: void setup(const Descriptor &descriptor); void setupWindow(const Descriptor &descriptor); - void setupWebview(const Descriptor &descriptor); + void setupWebview(); void processKey(const QString &key, const QString &modifier); void resolveCurrentLocation(); void resolveAddressByTimer(); @@ -129,11 +131,17 @@ private: std::unique_ptr _window; not_null _body; RpWidget *_container = nullptr; + RpWidget *_mapPlaceholder = nullptr; + RpWidget *_mapLoading = nullptr; + AbstractButton *_mapButton = nullptr; SlideWrap *_mapControlsWrap = nullptr; + rpl::variable _chooseButtonLabel; ScrollArea *_scroll = nullptr; + Webview::StorageId _webviewStorageId; std::unique_ptr _webview; SingleQueuedInvokation _updateStyles; Core::GeoLocation _initialProvided; + int _mapPlaceholderAdded = 0; bool _subscribedToColors = false; base::Timer _geocoderResolveTimer;