diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6f3bc7a002..592f1329ff 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1472,6 +1472,8 @@ PRIVATE ui/chat/choose_send_as.h ui/chat/choose_theme_controller.cpp ui/chat/choose_theme_controller.h + ui/controls/location_picker.cpp + ui/controls/location_picker.h ui/controls/silent_toggle.cpp ui/controls/silent_toggle.h ui/controls/userpic_button.cpp @@ -1493,6 +1495,10 @@ PRIVATE ui/image/image_location.h ui/image/image_location_factory.cpp ui/image/image_location_factory.h + ui/text/format_song_document_name.cpp + ui/text/format_song_document_name.h + ui/widgets/label_with_custom_emoji.cpp + ui/widgets/label_with_custom_emoji.h ui/countryinput.cpp ui/countryinput.h ui/dynamic_thumbnails.cpp @@ -1506,10 +1512,6 @@ PRIVATE ui/resize_area.h ui/search_field_controller.cpp ui/search_field_controller.h - ui/text/format_song_document_name.cpp - ui/text/format_song_document_name.h - ui/widgets/label_with_custom_emoji.cpp - ui/widgets/label_with_custom_emoji.h ui/unread_badge.cpp ui/unread_badge.h window/main_window.cpp diff --git a/Telegram/Resources/icons/chat/filled_location.png b/Telegram/Resources/icons/chat/filled_location.png new file mode 100644 index 0000000000..12cd2dcc81 Binary files /dev/null and b/Telegram/Resources/icons/chat/filled_location.png differ diff --git a/Telegram/Resources/icons/chat/filled_location@2x.png b/Telegram/Resources/icons/chat/filled_location@2x.png new file mode 100644 index 0000000000..cdef3f274a Binary files /dev/null and b/Telegram/Resources/icons/chat/filled_location@2x.png differ diff --git a/Telegram/Resources/icons/chat/filled_location@3x.png b/Telegram/Resources/icons/chat/filled_location@3x.png new file mode 100644 index 0000000000..11caf17cad Binary files /dev/null and b/Telegram/Resources/icons/chat/filled_location@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 122260d220..b15436b2b0 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3195,6 +3195,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_maps_point" = "Location"; "lng_maps_point_send" = "Send This Location"; +"lng_maps_or_choose" = "Or choose a venue"; +"lng_maps_places_in_area" = "Places in this area"; +"lng_maps_no_places" = "No places found"; "lng_live_location" = "Live Location"; "lng_live_location_now" = "updated just now"; "lng_live_location_minutes#one" = "updated {count} minute ago"; diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js index 54eb60abae..cb936cf0b0 100644 --- a/Telegram/Resources/picker_html/picker.js +++ b/Telegram/Resources/picker_html/picker.js @@ -31,10 +31,22 @@ var LocationPicker = { }); } }, + isNight: function() { + var html = document.getElementsByTagName('html')[0]; + return html.style.getPropertyValue('--td-night') == '1'; + }, + lightPreset: function() { + return LocationPicker.isNight() ? 'night' : 'day'; + }, updateStyles: function (styles) { if (LocationPicker.styles !== styles) { LocationPicker.styles = styles; document.getElementsByTagName('html')[0].style = styles; + + LocationPicker.map.setConfigProperty( + 'basemap', + 'lightPreset', + LocationPicker.lightPreset()); } }, init: function (params) { @@ -43,7 +55,9 @@ var LocationPicker = { mapboxgl.config.API_URL = params.protocol + '://domain/api.mapbox.com'; } - var options = { container: 'map' }; + var options = { container: 'map', config: { + basemap: { lightPreset: LocationPicker.lightPreset() } + } }; var center = params.center; if (center) { center = [center[1], center[0]]; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 8c7bcbb937..452a9f89b7 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1426,4 +1426,53 @@ paidTagPadding: margins(16px, 6px, 16px, 6px); pickLocationWindow: size(364px, 680px); pickLocationMapHeight: 220px; -pickLocationCollapsedHeight: 108px; +pickLocationCollapsedHeight: 92px; +pickLocationRowHeight: 52px; +pickLocationVenue: PeerListItem(defaultPeerListItem) { + height: pickLocationRowHeight; + photoSize: 42px; + photoPosition: point(18px, 5px); + namePosition: point(70px, 9px); + statusPosition: point(70px, 29px); + button: OutlineButton(defaultPeerListButton) { + textBg: contactsBg; + textBgOver: contactsBgOver; + ripple: defaultRippleAnimation; + } + statusFg: contactsStatusFg; + statusFgOver: contactsStatusFgOver; + statusFgActive: contactsStatusFgOnline; +} +pickLocationButton: FlatButton { + height: pickLocationRowHeight; + bgColor: contactsBg; + overBgColor: contactsBgOver; + ripple: defaultRippleAnimation; +} +pickLocationButtonText: FlatLabel(defaultFlatLabel) { + minWidth: 128px; + style: semiboldTextStyle; + textFg: windowBoldFg; +} +pickLocationButtonStatus: FlatLabel(defaultFlatLabel) { + minWidth: 128px; + textFg: windowSubTextFg; +} +pickLocationButtonSkip: 6px; +pickLocationSendIcon: icon{{ "chat/filled_location", windowFgActive }}; +pickLocationVenueItem: PeerListItem(defaultPeerListItem) { + button: OutlineButton(defaultPeerListButton) { + font: normalFont; + padding: margins(11px, 5px, 11px, 5px); + } + height: 52px; + photoPosition: point(18px, 5px); + namePosition: point(70px, 7px); + statusPosition: point(70px, 27px); + photoSize: 42px; +} +pickLocationVenueList: PeerList(defaultPeerList) { + item: pickLocationVenueItem; + padding: margins(0px, 0px, 0px, 0px); +} +pickLocationIconSkip: 6px; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 2021e29434..f2ad99f46f 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "data/stickers/data_stickers.h" #include "menu/menu_send.h" // SendMenu::FillSendMenu +#include "mtproto/mtproto_config.h" #include "core/click_handler_types.h" #include "ui/controls/tabbed_search.h" #include "ui/widgets/buttons.h" @@ -48,7 +49,6 @@ namespace ChatHelpers { namespace { constexpr auto kSearchRequestDelay = 400; -constexpr auto kSearchBotUsername = "gif"_cs; constexpr auto kMinRepaintDelay = crl::time(33); constexpr auto kMinAfterScrollDelay = crl::time(33); @@ -864,13 +864,11 @@ void GifsListWidget::searchForGifs(const QString &query) { } if (!_searchBot && !_searchBotRequestId) { - auto username = kSearchBotUsername.utf16(); + const auto username = session().serverConfig().gifSearchUsername; _searchBotRequestId = _api.request(MTPcontacts_ResolveUsername( MTP_string(username) )).done([=](const MTPcontacts_ResolvedPeer &result) { - Expects(result.type() == mtpc_contacts_resolvedPeer); - - auto &data = result.c_contacts_resolvedPeer(); + auto &data = result.data(); session().data().processUsers(data.vusers()); session().data().processChats(data.vchats()); const auto peer = session().data().peerLoaded( diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp index eb2125bf54..b649ea2ea7 100644 --- a/Telegram/SourceFiles/core/current_geo_location.cpp +++ b/Telegram/SourceFiles/core/current_geo_location.cpp @@ -8,10 +8,122 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/current_geo_location.h" #include "base/platform/base_platform_info.h" +#include "base/invoke_queued.h" +#include "base/timer.h" #include "data/raw/raw_countries_bounds.h" #include "platform/platform_current_geo_location.h" +#include "ui/ui_utility.h" + +#include +#include +#include +#include +#include +#include +#include namespace Core { +namespace { + +constexpr auto kDestroyManagerTimeout = 20 * crl::time(1000); + +void ResolveLocationAddressGeneric( + const GeoLocation &location, + const QString &token, + Fn callback) { + const auto partialUrl = u"https://api.mapbox.com/search/geocode/v6" + "/reverse?longitude=%1&latitude=%2&access_token=%3"_q + .arg(location.point.y()) + .arg(location.point.x()); + static auto Cache = base::flat_map(); + const auto i = Cache.find(partialUrl); + if (i != end(Cache)) { + callback(i->second); + return; + } + const auto finishWith = [=](GeoAddress result) { + Cache[partialUrl] = result; + callback(result); + }; + + struct State final : QObject { + explicit State(QObject *parent) + : QObject(parent) + , manager(this) + , destroyer([=] { if (sent.empty()) delete this; }) { + } + + QNetworkAccessManager manager; + std::vector> sent; + base::Timer destroyer; + }; + + static auto state = QPointer(); + if (!state) { + state = Ui::CreateChild(qApp); + } + const auto destroyReplyDelayed = [](QNetworkReply *reply) { + InvokeQueued(reply, [=] { + for (auto i = begin(state->sent); i != end(state->sent);) { + if (!*i || *i == reply) { + i = state->sent.erase(i); + } else { + ++i; + } + } + delete reply; + if (state->sent.empty()) { + state->destroyer.callOnce(kDestroyManagerTimeout); + } + }); + }; + + auto request = QNetworkRequest(partialUrl.arg(token)); + request.setRawHeader("Referer", "http://desktop-app-resource/"); + + const auto reply = state->manager.get(request); + QObject::connect(reply, &QNetworkReply::finished, [=] { + destroyReplyDelayed(reply); + + const auto json = QJsonDocument::fromJson(reply->readAll()); + if (!json.isObject()) { + finishWith({}); + return; + } + const auto features = json["features"].toArray(); + if (features.isEmpty()) { + finishWith({}); + return; + } + const auto feature = features.at(0).toObject(); + const auto properties = feature["properties"].toObject(); + const auto context = properties["context"].toObject(); + auto names = QStringList(); + auto add = [&](std::vector keys) { + for (const auto &key : keys) { + const auto value = context[key]; + if (value.isObject()) { + const auto name = value.toObject()["name"].toString(); + if (!name.isEmpty()) { + names.push_back(name); + break; + } + } + } + }; + add({ u"address"_q, u"street"_q, u"neighborhood"_q }); + add({ u"place"_q, u"region"_q }); + add({ u"country"_q }); + finishWith({ .name = names.join(", ") }); + }); + QObject::connect(reply, &QNetworkReply::errorOccurred, [=] { + destroyReplyDelayed(reply); + + finishWith({}); + }); +} + +} // namespace GeoLocation ResolveCurrentCountryLocation() { const auto iso2 = Platform::SystemCountry().toUpper(); @@ -47,4 +159,18 @@ void ResolveCurrentGeoLocation(Fn callback) { }); } +void ResolveLocationAddress( + const GeoLocation &location, + const QString &token, + Fn callback) { + auto done = [=, done = std::move(callback)](GeoAddress result) mutable { + if (!result && !token.isEmpty()) { + ResolveLocationAddressGeneric(location, token, std::move(done)); + } else { + done(result); + } + }; + Platform::ResolveLocationAddress(location, std::move(done)); +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h index 2715699eeb..e3b92222e9 100644 --- a/Telegram/SourceFiles/core/current_geo_location.h +++ b/Telegram/SourceFiles/core/current_geo_location.h @@ -33,9 +33,29 @@ struct GeoLocation { explicit operator bool() const { return !failed(); } + + friend inline bool operator==( + const GeoLocation&, + const GeoLocation&) = default; +}; + +struct GeoAddress { + QString name; + + [[nodiscard]] bool empty() const { + return name.isEmpty(); + } + explicit operator bool() const { + return !empty(); + } }; [[nodiscard]] GeoLocation ResolveCurrentCountryLocation(); void ResolveCurrentGeoLocation(Fn callback); +void ResolveLocationAddress( + const GeoLocation &location, + const QString &token, + Fn callback); + } // namespace Core diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h index 6fb00d550d..10f05adfbe 100644 --- a/Telegram/SourceFiles/data/data_location.h +++ b/Telegram/SourceFiles/data/data_location.h @@ -53,6 +53,10 @@ struct InputVenue { QString provider; QString id; QString venueType; + + friend inline bool operator==( + const InputVenue &, + const InputVenue &) = default; }; [[nodiscard]] GeoPointLocation ComputeLocation(const LocationPoint &point); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 869476243c..e43e6423ad 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -3328,6 +3328,22 @@ void Session::documentApplyFields( } } +not_null Session::venueIconDocument(const QString &icon) { + const auto i = _venueIcons.find(icon); + if (i != end(_venueIcons)) { + return i->second; + } + const auto result = documentFromWeb(MTP_webDocumentNoProxy( + MTP_string(u"https://ss3.4sqi.net/img/categories_v2/"_q + + icon + + u"_64.png"_q), + MTP_int(0), + MTP_string("image/png"), + MTP_vector()), {}, {}); + _venueIcons.emplace(icon, result); + return result; +} + not_null Session::webpage(WebPageId id) { auto i = _webpages.find(id); if (i == _webpages.cend()) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 12c4cba3a6..06b4b267d3 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -559,6 +559,8 @@ public: const MTPWebDocument &data, const ImageLocation &thumbnailLocation, const ImageLocation &videoThumbnailLocation); + [[nodiscard]] not_null venueIconDocument( + const QString &icon); [[nodiscard]] not_null webpage(WebPageId id); not_null processWebpage(const MTPWebPage &data); @@ -1002,6 +1004,7 @@ private: FullStoryId, base::flat_set>> _storyItems; base::flat_map> _highlightings; + base::flat_map> _venueIcons; base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index c6b0139560..b9233313ce 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -162,6 +162,10 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000); return u""_q; } +[[nodiscard]] QString ResolveGeocodingToken(not_null session) { + return u""_q; +} + void ShowChooseBox( not_null controller, PeerTypes types, @@ -1808,6 +1812,7 @@ void ChooseAndSendLocation( }; Ui::LocationPicker::Show({ .parent = controller->widget(), + .session = &controller->session(), .callback = crl::guard(controller, callback), .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, .storageId = controller->session().local().resolveStorageIdBots(), @@ -1872,7 +1877,9 @@ std::unique_ptr MakeAttachBotsMenu( const auto session = &controller->session(); const auto locationType = ChatRestriction::SendOther; if (Data::CanSendAnyOf(peer, locationType) - && Ui::LocationPicker::Available(ResolveMapsToken(session))) { + && Ui::LocationPicker::Available( + ResolveMapsToken(session), + ResolveGeocodingToken(session))) { raw->addAction(tr::lng_maps_point(tr::now), [=] { ChooseAndSendLocation(controller, actionFactory()); }, &st::menuIconAddress); diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index 3b30dc6dbb..4161fe8762 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/fade_wrap.h" #include "ui/basic_click_handlers.h" #include "ui/painter.h" +#include "ui/webview_helpers.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_embed.h" #include "webview/webview_interface.h" @@ -39,8 +40,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#include "base/call_delayed.h" - namespace Iv { namespace { @@ -79,67 +78,7 @@ namespace { static const auto phrases = base::flat_map>{ { "iv-join-channel", tr::lng_iv_join_channel }, }; - static const auto serialize = [](const style::color *color) { - const auto qt = (*color)->c; - if (qt.alpha() == 255) { - return '#' - + QByteArray::number(qt.red(), 16).right(2) - + QByteArray::number(qt.green(), 16).right(2) - + QByteArray::number(qt.blue(), 16).right(2); - } - return "rgba(" - + QByteArray::number(qt.red()) + "," - + QByteArray::number(qt.green()) + "," - + QByteArray::number(qt.blue()) + "," - + QByteArray::number(qt.alpha() / 255.) + ")"; - }; - static const auto escape = [](tr::phrase<> phrase) { - const auto text = phrase(tr::now); - - auto result = QByteArray(); - for (auto i = 0; i != text.size(); ++i) { - uint ucs4 = text[i].unicode(); - if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) { - ushort low = text[i + 1].unicode(); - if (QChar::isLowSurrogate(low)) { - ucs4 = QChar::surrogateToUcs4(ucs4, low); - ++i; - } - } - if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') { - result.append('\\').append(char(ucs4)); - } else if (ucs4 < 32 || ucs4 > 127) { - result.append('\\' + QByteArray::number(ucs4, 16) + ' '); - } else { - result.append(char(ucs4)); - } - } - return result; - }; - auto result = QByteArray(); - for (const auto &[name, phrase] : phrases) { - result += "--td-lng-" + name + ":'" + escape(phrase) + "'; "; - } - for (const auto &[name, color] : map) { - result += "--td-" + name + ':' + serialize(color) + ';'; - } - return result; -} - -[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) { - return value - .replace('&', "&") - .replace('"', """) - .replace('\'', "'") - .replace('<', "<") - .replace('>', ">"); -} - -[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) { - return value - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('\'', "\\\'"); + return Ui::ComputeStyles(map, phrases); } [[nodiscard]] QByteArray WrapPage(const Prepared &page) { @@ -159,7 +98,7 @@ namespace { @@ -194,7 +133,7 @@ Controller::Controller( Fn showShareBox) : _delegate(delegate) , _updateStyles([=] { - const auto str = EscapeForScriptString(ComputeStyles()); + const auto str = Ui::EscapeForScriptString(ComputeStyles()); if (_webview) { _webview->eval("IV.updateStyles('" + str + "');"); } @@ -612,7 +551,7 @@ QByteArray Controller::navigateScript(int index, const QString &hash) { return "IV.navigateTo(" + QByteArray::number(index) + ", '" - + EscapeForScriptString(qthelp::url_decode(hash).toUtf8()) + + Ui::EscapeForScriptString(qthelp::url_decode(hash).toUtf8()) + "');"; } @@ -679,7 +618,7 @@ bool Controller::active() const { void Controller::showJoinedTooltip() { if (_webview && _ready) { _webview->eval("IV.showTooltip('" - + EscapeForScriptString( + + Ui::EscapeForScriptString( tr::lng_action_you_joined(tr::now).toUtf8()) + "');"); } diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index ea61115740..c14d490511 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -73,10 +73,14 @@ constexpr auto kTmpPasswordReserveTime = TimeId(10); if (domain.startsWith(prefix, Qt::CaseInsensitive)) { return domain.endsWith('/') ? domain - : MTP::ConfigFields().internalLinksDomain; + : MTP::ConfigFields( + session->mtp().environment() + ).internalLinksDomain; } } - return MTP::ConfigFields().internalLinksDomain; + return MTP::ConfigFields( + session->mtp().environment() + ).internalLinksDomain; } } // namespace diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.cpp b/Telegram/SourceFiles/mtproto/mtproto_config.cpp index 454339f494..1da5325ef8 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_config.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_config.cpp @@ -16,18 +16,30 @@ namespace { constexpr auto kVersion = 1; -} // namespace - -QString ConfigDefaultReactionEmoji() { +[[nodiscard]] QString ConfigDefaultReactionEmoji() { static const auto result = QString::fromUtf8("\xf0\x9f\x91\x8d"); return result; } -Config::Config(Environment environment) : _dcOptions(environment) { - _fields.webFileDcId = _dcOptions.isTestMode() ? 2 : 4; - _fields.txtDomainString = _dcOptions.isTestMode() - ? u"tapv3.stel.com"_q - : u"apv3.stel.com"_q; +} // namespace + +ConfigFields::ConfigFields(Environment environment) +: webFileDcId(environment == Environment::Test ? 2 : 4) +, txtDomainString(environment == Environment::Test + ? u"tapv3.stel.com"_q + : u"apv3.stel.com"_q) +, reactionDefaultEmoji(ConfigDefaultReactionEmoji()) +, gifSearchUsername(environment == Environment::Test + ? u"izgifbot"_q + : u"gif"_q) +, venueSearchUsername(environment == Environment::Test + ? u"foursquarebot"_q + : u"foursquare"_q) { +} + +Config::Config(Environment environment) +: _dcOptions(environment) +, _fields(environment) { } Config::Config(const Config &other) @@ -46,7 +58,9 @@ QByteArray Config::serialize() const { + 3 * sizeof(qint32) + Serialize::stringSize(_fields.reactionDefaultEmoji) + sizeof(quint64) - + sizeof(qint32); + + sizeof(qint32) + + Serialize::stringSize(_fields.gifSearchUsername) + + Serialize::stringSize(_fields.venueSearchUsername); auto result = QByteArray(); result.reserve(size); @@ -91,7 +105,9 @@ QByteArray Config::serialize() const { << qint32(_fields.captionLengthMax) << _fields.reactionDefaultEmoji << quint64(_fields.reactionDefaultCustom) - << qint32(_fields.ratingDecay); + << qint32(_fields.ratingDecay) + << _fields.gifSearchUsername + << _fields.venueSearchUsername; } return result; } @@ -190,6 +206,10 @@ std::unique_ptr Config::FromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { read(raw->_fields.ratingDecay); } + if (!stream.atEnd()) { + read(raw->_fields.gifSearchUsername); + read(raw->_fields.venueSearchUsername); + } if (stream.status() != QDataStream::Ok || !raw->_dcOptions.constructFromSerialized(dcOptionsSerialized)) { @@ -256,8 +276,12 @@ void Config::apply(const MTPDconfig &data) { _fields.autologinToken = qs(data.vautologin_token().value_or_empty()); _fields.ratingDecay = data.vrating_e_decay().v; if (_fields.ratingDecay <= 0) { - _fields.ratingDecay = ConfigFields().ratingDecay; + _fields.ratingDecay = ConfigFields( + _dcOptions.environment() + ).ratingDecay; } + _fields.gifSearchUsername = qs(data.vgif_search_username().value_or_empty()); + _fields.venueSearchUsername = qs(data.vvenue_search_username().value_or_empty()); if (data.vdc_options().v.empty()) { LOG(("MTP Error: config with empty dc_options received!")); diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.h b/Telegram/SourceFiles/mtproto/mtproto_config.h index 8a4db80df9..289230b644 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_config.h +++ b/Telegram/SourceFiles/mtproto/mtproto_config.h @@ -11,9 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace MTP { -[[nodiscard]] QString ConfigDefaultReactionEmoji(); - struct ConfigFields { + explicit ConfigFields(Environment environment); + int chatSizeMax = 200; int megagroupSizeMax = 10000; int forwardedCountMax = 100; @@ -40,9 +40,12 @@ struct ConfigFields { bool blockedMode = false; int captionLengthMax = 1024; int ratingDecay = 2419200; - QString reactionDefaultEmoji = ConfigDefaultReactionEmoji(); - uint64 reactionDefaultCustom; + QString reactionDefaultEmoji; + uint64 reactionDefaultCustom = 0; QString autologinToken; + + QString gifSearchUsername; + QString venueSearchUsername; }; class Config final { diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp index 5ac0c49a78..6f42c0afdb 100644 --- a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp @@ -14,5 +14,10 @@ namespace Platform { void ResolveCurrentExactLocation(Fn callback) { callback({}); } +void ResolveLocationAddress( + const Core::GeoLocation &location, + Fn callback) { + callback({}); +} } // namespace Platform diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm index 2812a0c333..452bc6e3ae 100644 --- a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm +++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm @@ -116,4 +116,10 @@ void ResolveCurrentExactLocation(Fn callback) { [[LocationDelegate alloc] initWithCallback:std::move(callback)]; } +void ResolveLocationAddress( + const Core::GeoLocation &location, + Fn callback) { + callback({}); +} + } // namespace Platform diff --git a/Telegram/SourceFiles/platform/platform_current_geo_location.h b/Telegram/SourceFiles/platform/platform_current_geo_location.h index 245342fc36..9feb4b376e 100644 --- a/Telegram/SourceFiles/platform/platform_current_geo_location.h +++ b/Telegram/SourceFiles/platform/platform_current_geo_location.h @@ -9,10 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Core { struct GeoLocation; +struct GeoAddress; } // namespace Core namespace Platform { void ResolveCurrentExactLocation(Fn callback); +void ResolveLocationAddress( + const Core::GeoLocation &location, + Fn callback); } // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp index 37682c108d..a83dedb882 100644 --- a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp +++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp @@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include +#include + namespace Platform { void ResolveCurrentExactLocation(Fn callback) { @@ -56,4 +59,10 @@ void ResolveCurrentExactLocation(Fn callback) { } } +void ResolveLocationAddress( + const Core::GeoLocation &location, + Fn callback) { + callback({}); +} + } // namespace Platform diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp index 5b77f017ad..a4658e7bee 100644 --- a/Telegram/SourceFiles/ui/controls/location_picker.cpp +++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp @@ -7,16 +7,31 @@ 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 "lang/lang_keys.h" +#include "main/session/session_show.h" +#include "main/main_session.h" +#include "mtproto/mtproto_config.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/separate_panel.h" #include "ui/widgets/buttons.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" @@ -39,6 +54,246 @@ const auto kProtocolOverride = ""; Core::GeoLocation LastExactLocation; QString MapsProviderToken; +QString GeocodingProviderToken; + +using VenueData = Data::InputVenue; + +class VenueRowDelegate { +public: + virtual void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + const QString &type) = 0; +}; + +class VenueRow final : public PeerListRow { +public: + VenueRow(not_null delegate, const VenueData &data); + + void update(const VenueData &data); + + [[nodiscard]] VenueData data() const; + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + +private: + const not_null _delegate; + VenueData _data; + +}; + +VenueRow::VenueRow( + not_null delegate, + const VenueData &data) +: PeerListRow(UniqueRowIdFromString(data.id)) +, _delegate(delegate) +, _data(data) { + setCustomStatus(data.address); +} + +void VenueRow::update(const VenueData &data) { + _data = data; + setCustomStatus(data.address); +} + +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 LinksController final + : public PeerListController + , public VenueRowDelegate + , public base::has_weak_ptr { +public: + LinksController( + not_null session, + rpl::producer> content); + + void prepare() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + Main::Session &session() const override; + + void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + const QString &type) override; + +private: + struct VenueIcon { + not_null document; + std::shared_ptr media; + uint32 paletteVersion : 31 = 0; + uint32 iconLoaded : 1 = 0; + QImage image; + QImage icon; + }; + + void appendRow(const VenueData &data); + + void rebuild(const std::vector &rows); + + const not_null _session; + rpl::variable> _rows; + + base::flat_map _icons; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] QString NormalizeVenuesQuery(QString query) { + return query.trimmed().toLower(); +} + +LinksController::LinksController( + not_null session, + rpl::producer> content) +: _session(session) +, _rows(std::move(content)) { +} + +void LinksController::prepare() { + _rows.value( + ) | rpl::start_with_next([=](const std::vector &rows) { + rebuild(rows); + }, _lifetime); +} + +void LinksController::rebuild(const std::vector &rows) { + auto i = 0; + auto count = delegate()->peerListFullRowsCount(); + while (i < rows.size()) { + if (i < count) { + const auto row = delegate()->peerListRowAt(i); + static_cast(row.get())->update(rows[i]); + } else { + appendRow(rows[i]); + } + ++i; + } + while (i < count) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(i)); + --count; + } + delegate()->peerListRefreshRows(); +} + +void LinksController::rowClicked(not_null row) { + const auto venue = static_cast(row.get())->data(); + venue; +} + +void LinksController::rowRightActionClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +Main::Session &LinksController::session() const { + return *_session; +} + +void LinksController::appendRow(const VenueData &data) { + delegate()->peerListAppendRow(std::make_unique(this, data)); +} + +void LinksController::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); + } + } + + 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() { if (!LastExactLocation) { @@ -68,23 +323,16 @@ QString MapsProviderToken; } [[nodiscard]] QByteArray ComputeStyles() { - return ""; -} - -[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) { - return value - .replace('&', "&") - .replace('"', """) - .replace('\'', "'") - .replace('<', "<") - .replace('>', ">"); -} - -[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) { - return value - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('\'', "\\\'"); + static const auto map = base::flat_map{ + { "window-bg", &st::windowBg }, + { "window-bg-over", &st::windowBgOver }, + { "window-bg-ripple", &st::windowBgRipple }, + { "window-active-text-fg", &st::windowActiveTextFg }, + }; + static const auto phrases = base::flat_map>{ + { "maps-places-in-area", tr::lng_maps_places_in_area }, + }; + return Ui::ComputeStyles(map, phrases, Window::Theme::IsNightMode()); } [[nodiscard]] QByteArray ReadResource(const QString &name) { @@ -115,6 +363,129 @@ QString MapsProviderToken; )"_q; } +[[nodiscard]] object_ptr MakeSendLocationButton( + QWidget *parent, + rpl::producer address) { + auto result = object_ptr( + parent, + QString(), + st::pickLocationButton); + const auto raw = result.data(); + + const auto st = &st::pickLocationVenue; + const auto icon = CreateChild(raw); + icon->setGeometry( + st->photoPosition.x(), + st->photoPosition.y(), + st->photoSize, + st->photoSize); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgActive); + p.drawEllipse(icon->rect()); + st::pickLocationSendIcon.paintInCenter(p, icon->rect()); + }, icon->lifetime()); + icon->show(); + + const auto hadAddress = std::make_shared(false); + auto statusText = std::move( + address + ) | rpl::map([=](const QString &text) { + if (!text.isEmpty()) { + *hadAddress = true; + return text; + } + return *hadAddress ? tr::lng_contacts_loading(tr::now) : QString(); + }); + const auto name = CreateChild( + raw, + tr::lng_maps_point_send(tr::now), + st::pickLocationButtonText); + name->show(); + const auto status = CreateChild( + raw, + rpl::duplicate(statusText), + st::pickLocationButtonStatus); + status->showOn(std::move( + statusText + ) | rpl::map([](const QString &text) { + return !text.isEmpty(); + }) | rpl::distinct_until_changed()); + rpl::combine( + result->widthValue(), + status->shownValue() + ) | rpl::start_with_next([=](int width, bool statusShown) { + 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 = statusShown + ? namePosition.y() + : (st->height - name->height()) / 2; + name->moveToLeft(namePosition.x(), nameTop, width); + status->resizeToWidth(available); + status->moveToLeft(statusPosition.x(), statusPosition.y(), width); + }, name->lifetime()); + + return result; +} + +void SetupVenues( + not_null container, + std::shared_ptr show, + rpl::producer> value) { + auto &lifetime = container->lifetime(); + const auto delegate = lifetime.make_state( + show); + const auto controller = lifetime.make_state( + &show->session(), + std::move(value)); + controller->setStyleOverrides(&st::pickLocationVenueList); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); + + show->session().downloaderTaskFinished() | rpl::start_with_next([=] { + content->update(); + }, content->lifetime()); +} + +[[nodiscard]] PickerVenueList ParseVenues( + not_null session, + const MTPmessages_BotResults &venues) { + const auto &data = venues.data(); + session->data().processUsers(data.vusers()); + + auto &list = data.vresults().v; + auto result = PickerVenueList(); + result.list.reserve(list.size()); + for (const auto &found : list) { + found.match([&](const auto &data) { + data.vsend_message().match([&]( + const MTPDbotInlineMessageMediaVenue &data) { + data.vgeo().match([&](const MTPDgeoPoint &geo) { + result.list.push_back({ + .lat = geo.vlat().v, + .lon = geo.vlong().v, + .title = qs(data.vtitle()), + .address = qs(data.vaddress()), + .provider = qs(data.vprovider()), + .id = qs(data.vvenue_id()), + .venueType = qs(data.vvenue_type()), + }); + }, [](const auto &) {}); + }, [](const auto &) {}); + }); + } + return result; +} + } // namespace LocationPicker::LocationPicker(Descriptor &&descriptor) @@ -127,9 +498,12 @@ LocationPicker::LocationPicker(Descriptor &&descriptor) , _updateStyles([=] { const auto str = EscapeForScriptString(ComputeStyles()); if (_webview) { - _webview->eval("IV.updateStyles('" + str + "');"); + _webview->eval("LocationPicker.updateStyles('" + str + "');"); } -}) { +}) +, _venueState(PickerVenueLoading()) +, _session(descriptor.session) +, _api(&_session->mtp()) { std::move( descriptor.closeRequests ) | rpl::start_with_next([=] { @@ -140,15 +514,26 @@ LocationPicker::LocationPicker(Descriptor &&descriptor) setup(descriptor); } -bool LocationPicker::Available(const QString &token) { +std::shared_ptr LocationPicker::uiShow() { + return Main::MakeSessionShow(nullptr, _session); +} + +bool LocationPicker::Available( + const QString &mapsToken, + const QString &geocodingToken) { static const auto Supported = Webview::NavigateToDataSupported(); - MapsProviderToken = token; + MapsProviderToken = mapsToken; + GeocodingProviderToken = geocodingToken; return Supported && !MapsProviderToken.isEmpty(); } void LocationPicker::setup(const Descriptor &descriptor) { setupWindow(descriptor); setupWebview(descriptor); + if (LastExactLocation) { + venuesRequest(LastExactLocation); + resolveAddress(LastExactLocation); + } } void LocationPicker::setupWindow(const Descriptor &descriptor) { @@ -168,24 +553,32 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { parent.y() + (parent.height() - window->height()) / 2); _container = CreateChild(_body.get()); - const auto scroll = CreateChild(_body.get()); - const auto controls = scroll->setOwnedWidget( - object_ptr(scroll)); + _scroll = CreateChild(_body.get()); + const auto controls = _scroll->setOwnedWidget( + object_ptr(_scroll)); const auto toppad = controls->add(object_ptr(controls)); - const auto button = controls->add(object_ptr( - controls, - tr::lng_maps_point_send(tr::now), - st::dialogsUpdateButton)); + const auto button = controls->add( + MakeSendLocationButton(controls, _geocoderAddress.value()), + { 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip }); button->setClickedCallback([=] { _webview->eval("LocationPicker.send();"); }); - controls->add(object_ptr(controls))->resize( - st::pickLocationWindow); + + AddDivider(controls); + AddSkip(controls); + AddSubsectionTitle(controls, tr::lng_maps_or_choose()); + + SetupVenues(controls, uiShow(), _venueState.value( + ) | rpl::filter([=](const PickerVenueState &state) { + return v::is(state); + }) | rpl::map([=](PickerVenueState &&state) { + return std::move(v::get(state).list); + })); rpl::combine( _body->sizeValue(), - scroll->scrollTopValue() + _scroll->scrollTopValue() ) | rpl::start_with_next([=](QSize size, int scrollTop) { const auto width = size.width(); const auto height = size.height(); @@ -194,9 +587,9 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { scrollTop); const auto mapHeight = st::pickLocationMapHeight - sub; const auto scrollHeight = height - mapHeight; - button->resizeToWidth(width); _container->setGeometry(0, 0, width, mapHeight); - scroll->setGeometry(0, mapHeight, width, scrollHeight); + _scroll->setGeometry(0, mapHeight, width, scrollHeight); + controls->resizeToWidth(width); toppad->resize(width, sub); }, _container->lifetime()); @@ -205,7 +598,7 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) { }, _container->lifetime()); _container->show(); - scroll->show(); + _scroll->hide(); controls->show(); button->show(); window->show(); @@ -256,7 +649,7 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) { const auto object = message.object(); const auto event = object.value("event").toString(); if (event == u"ready"_q) { - initMap(); + mapReady(); resolveCurrentLocation(); } else if (event == u"keydown"_q) { const auto key = object.value("key").toString(); @@ -321,7 +714,31 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) { raw->navigateToData("location/picker.html"); } -void LocationPicker::initMap() { +void LocationPicker::resolveAddress(Core::GeoLocation location) { + if (_geocoderResolvingFor == location) { + return; + } + _geocoderResolvingFor = location; + const auto done = [=](Core::GeoAddress address) { + if (_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'); + } + }; + Core::ResolveLocationAddress( + location, + GeocodingProviderToken, + crl::guard(this, done)); +} + +void LocationPicker::mapReady() { + Expects(_scroll != nullptr); + const auto token = MapsProviderToken.toUtf8(); const auto center = DefaultCenter(); const auto bounds = DefaultBounds(); @@ -333,16 +750,103 @@ void LocationPicker::initMap() { + ", bounds: " + bounds + ", protocol: " + protocol; _webview->eval("LocationPicker.init({ " + params + " });"); + + _scroll->show(); +} + +void LocationPicker::venuesRequest( + Core::GeoLocation location, + QString query) { + query = NormalizeVenuesQuery(query); + auto &cache = _venuesCache[query]; + const auto i = ranges::find( + cache, + location, + &VenuesCacheEntry::location); + if (i != end(cache)) { + _venueState = i->result; + return; + } else if (_venuesRequestLocation == location + && _venuesRequestQuery == query) { + return; + } else if (const auto oldRequestId = base::take(_venuesRequestId)) { + _api.request(oldRequestId).cancel(); + } + _venueState = PickerVenueLoading(); + _venuesRequestLocation = location; + _venuesRequestQuery = query; + if (_venuesBot) { + venuesSendRequest(); + } else if (_venuesBotRequestId) { + return; + } + const auto username = _session->serverConfig().venueSearchUsername; + _venuesBotRequestId = _api.request(MTPcontacts_ResolveUsername( + MTP_string(username) + )).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, + 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, + }); + if (parsed.list.empty()) { + _venueState = PickerVenueNothingFound{ _venuesRequestQuery }; + } else { + _venueState = std::move(parsed); + } + }).fail([=] { + _venueState = PickerVenueNothingFound{ _venuesRequestQuery }; + }).send(); } void LocationPicker::resolveCurrentLocation() { using namespace Core; const auto window = _window.get(); ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) { - if (location.accuracy != GeoLocationAccuracy::Exact) { + const auto changed = (LastExactLocation != location); + if (location.accuracy != GeoLocationAccuracy::Exact || !changed) { return; } LastExactLocation = location; + if (location) { + venuesRequest(location); + resolveAddress(location); + } if (_webview) { const auto point = QByteArray::number(location.point.x()) + ","_q diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h index f18957b9dc..9a4ca56d59 100644 --- a/Telegram/SourceFiles/ui/controls/location_picker.h +++ b/Telegram/SourceFiles/ui/controls/location_picker.h @@ -9,8 +9,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/invoke_queued.h" #include "base/weak_ptr.h" +#include "core/current_geo_location.h" +#include "mtproto/sender.h" #include "webview/webview_common.h" +namespace Data { +struct InputVenue; +} // namespace Data + +namespace Main { +class Session; +class SessionShow; +} // namespace Main + namespace Webview { class Window; } // namespace Webview @@ -19,23 +30,61 @@ namespace Ui { class SeparatePanel; class RpWidget; +class ScrollArea; struct LocationInfo { float64 lat = 0.; float64 lon = 0.; }; +struct PickerVenueLoading { + friend inline bool operator==( + PickerVenueLoading, + PickerVenueLoading) = default; +}; + +struct PickerVenueNothingFound { + QString query; + + friend inline bool operator==( + const PickerVenueNothingFound&, + const PickerVenueNothingFound&) = default; +}; + +struct PickerVenueWaitingForLocation { + friend inline bool operator==( + PickerVenueWaitingForLocation, + PickerVenueWaitingForLocation) = default; +}; + +struct PickerVenueList { + std::vector list; + + friend inline bool operator==( + const PickerVenueList&, + const PickerVenueList&) = default; +}; + +using PickerVenueState = std::variant< + PickerVenueLoading, + PickerVenueNothingFound, + PickerVenueWaitingForLocation, + PickerVenueList>; + class LocationPicker final : public base::has_weak_ptr { public: struct Descriptor { RpWidget *parent = nullptr; + not_null session; Fn callback; Fn quit; Webview::StorageId storageId; rpl::producer<> closeRequests; }; - [[nodiscard]] static bool Available(const QString &token); + [[nodiscard]] static bool Available( + const QString &mapsToken, + const QString &geocodingToken); static not_null Show(Descriptor &&descriptor); void close(); @@ -43,14 +92,25 @@ public: void quit(); private: + struct VenuesCacheEntry { + Core::GeoLocation location; + PickerVenueList result; + }; + explicit LocationPicker(Descriptor &&descriptor); + [[nodiscard]] std::shared_ptr uiShow(); + void setup(const Descriptor &descriptor); void setupWindow(const Descriptor &descriptor); void setupWebview(const Descriptor &descriptor); void processKey(const QString &key, const QString &modifier); void resolveCurrentLocation(); - void initMap(); + void resolveAddress(Core::GeoLocation location); + void mapReady(); + + void venuesRequest(Core::GeoLocation location, QString query = {}); + void venuesSendRequest(); rpl::lifetime _lifetime; @@ -59,10 +119,25 @@ private: std::unique_ptr _window; not_null _body; RpWidget *_container = nullptr; + ScrollArea *_scroll = nullptr; std::unique_ptr _webview; SingleQueuedInvokation _updateStyles; bool _subscribedToColors = false; + Core::GeoLocation _geocoderResolvingFor; + rpl::variable _geocoderAddress; + + rpl::variable _venueState; + + const not_null _session; + MTP::Sender _api; + UserData *_venuesBot = nullptr; + mtpRequestId _venuesBotRequestId = 0; + mtpRequestId _venuesRequestId = 0; + Core::GeoLocation _venuesRequestLocation; + QString _venuesRequestQuery; + base::flat_map> _venuesCache; + }; } // namespace Ui diff --git a/Telegram/SourceFiles/ui/webview_helpers.cpp b/Telegram/SourceFiles/ui/webview_helpers.cpp new file mode 100644 index 0000000000..24b7155ce9 --- /dev/null +++ b/Telegram/SourceFiles/ui/webview_helpers.cpp @@ -0,0 +1,82 @@ +/* +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/webview_helpers.h" + +#include "lang/lang_keys.h" + +namespace Ui { + +[[nodiscard]] QByteArray ComputeStyles( + const base::flat_map &colors, + const base::flat_map> &phrases, + bool nightTheme) { + static const auto serialize = [](const style::color *color) { + const auto qt = (*color)->c; + if (qt.alpha() == 255) { + return '#' + + QByteArray::number(qt.red(), 16).right(2) + + QByteArray::number(qt.green(), 16).right(2) + + QByteArray::number(qt.blue(), 16).right(2); + } + return "rgba(" + + QByteArray::number(qt.red()) + "," + + QByteArray::number(qt.green()) + "," + + QByteArray::number(qt.blue()) + "," + + QByteArray::number(qt.alpha() / 255.) + ")"; + }; + static const auto escape = [](tr::phrase<> phrase) { + const auto text = phrase(tr::now); + + auto result = QByteArray(); + for (auto i = 0; i != text.size(); ++i) { + uint ucs4 = text[i].unicode(); + if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) { + ushort low = text[i + 1].unicode(); + if (QChar::isLowSurrogate(low)) { + ucs4 = QChar::surrogateToUcs4(ucs4, low); + ++i; + } + } + if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') { + result.append('\\').append(char(ucs4)); + } else if (ucs4 < 32 || ucs4 > 127) { + result.append('\\' + QByteArray::number(ucs4, 16) + ' '); + } else { + result.append(char(ucs4)); + } + } + return result; + }; + auto result = QByteArray(); + for (const auto &[name, phrase] : phrases) { + result += "--td-lng-"_q + name + ":'"_q + escape(phrase) + "'; "_q; + } + for (const auto &[name, color] : colors) { + result += "--td-"_q + name + ':' + serialize(color) + ';'; + } + result += "--td-night:"_q + (nightTheme ? "1" : "0") + ';'; + return result; +} + +QByteArray EscapeForAttribute(QByteArray value) { + return value + .replace('&', "&") + .replace('"', """) + .replace('\'', "'") + .replace('<', "<") + .replace('>', ">"); +} + +QByteArray EscapeForScriptString(QByteArray value) { + return value + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\'', "\\\'"); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/webview_helpers.h b/Telegram/SourceFiles/ui/webview_helpers.h new file mode 100644 index 0000000000..3d8a510669 --- /dev/null +++ b/Telegram/SourceFiles/ui/webview_helpers.h @@ -0,0 +1,27 @@ +/* +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/flat_map.h" + +namespace tr { +template +struct phrase; +} // namespace tr + +namespace Ui { + +[[nodiscard]] QByteArray ComputeStyles( + const base::flat_map &colors, + const base::flat_map> &phrases, + bool nightTheme = false); + +[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value); +[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value); + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index b11016acbd..9bc3f6c766 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -355,8 +355,6 @@ PRIVATE ui/controls/invite_link_buttons.h ui/controls/invite_link_label.cpp ui/controls/invite_link_label.h - ui/controls/location_picker.cpp - ui/controls/location_picker.h ui/controls/peer_list_dummy.cpp ui/controls/peer_list_dummy.h ui/controls/send_as_button.cpp @@ -403,6 +401,11 @@ PRIVATE ui/text/text_options.cpp ui/text/text_options.h + ui/widgets/fields/special_fields.cpp + ui/widgets/fields/special_fields.h + ui/widgets/fields/time_part_input_with_placeholder.cpp + ui/widgets/fields/time_part_input_with_placeholder.h + ui/widgets/color_editor.cpp ui/widgets/color_editor.h ui/widgets/continuous_sliders.cpp @@ -441,10 +444,8 @@ PRIVATE ui/unread_badge_paint.h ui/userpic_view.cpp ui/userpic_view.h - ui/widgets/fields/special_fields.cpp - ui/widgets/fields/special_fields.h - ui/widgets/fields/time_part_input_with_placeholder.cpp - ui/widgets/fields/time_part_input_with_placeholder.h + ui/webview_helpers.cpp + ui/webview_helpers.h window/window_slide_animation.cpp window/window_slide_animation.h