Add venues list and chosen place name.

This commit is contained in:
John Preston 2024-07-07 21:01:26 +04:00
parent 8e6d7bb190
commit 310837c9e1
27 changed files with 1066 additions and 141 deletions

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -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";

View file

@ -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]];

View file

@ -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;

View file

@ -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(

View file

@ -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 <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtCore/QCoreApplication>
#include <QtCore/QPointer>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
namespace Core {
namespace {
constexpr auto kDestroyManagerTimeout = 20 * crl::time(1000);
void ResolveLocationAddressGeneric(
const GeoLocation &location,
const QString &token,
Fn<void(GeoAddress)> 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<QString, GeoAddress>();
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<QPointer<QNetworkReply>> sent;
base::Timer destroyer;
};
static auto state = QPointer<State>();
if (!state) {
state = Ui::CreateChild<State>(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<QString> 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<void(GeoLocation)> callback) {
});
}
void ResolveLocationAddress(
const GeoLocation &location,
const QString &token,
Fn<void(GeoAddress)> 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

View file

@ -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<void(GeoLocation)> callback);
void ResolveLocationAddress(
const GeoLocation &location,
const QString &token,
Fn<void(GeoAddress)> callback);
} // namespace Core

View file

@ -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);

View file

@ -3328,6 +3328,22 @@ void Session::documentApplyFields(
}
}
not_null<DocumentData*> 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<MTPDocumentAttribute>()), {}, {});
_venueIcons.emplace(icon, result);
return result;
}
not_null<WebPageData*> Session::webpage(WebPageId id) {
auto i = _webpages.find(id);
if (i == _webpages.cend()) {

View file

@ -559,6 +559,8 @@ public:
const MTPWebDocument &data,
const ImageLocation &thumbnailLocation,
const ImageLocation &videoThumbnailLocation);
[[nodiscard]] not_null<DocumentData*> venueIconDocument(
const QString &icon);
[[nodiscard]] not_null<WebPageData*> webpage(WebPageId id);
not_null<WebPageData*> processWebpage(const MTPWebPage &data);
@ -1002,6 +1004,7 @@ private:
FullStoryId,
base::flat_set<not_null<HistoryItem*>>> _storyItems;
base::flat_map<uint64, not_null<HistoryItem*>> _highlightings;
base::flat_map<QString, not_null<DocumentData*>> _venueIcons;
base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
base::flat_set<not_null<GameData*>> _gamesUpdated;

View file

@ -162,6 +162,10 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
return u""_q;
}
[[nodiscard]] QString ResolveGeocodingToken(not_null<Main::Session*> session) {
return u""_q;
}
void ShowChooseBox(
not_null<Window::SessionController*> 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<Ui::DropdownMenu> 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);

View file

@ -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 <QtGui/QWindow>
#include <charconv>
#include "base/call_delayed.h"
namespace Iv {
namespace {
@ -79,67 +78,7 @@ namespace {
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
{ "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('&', "&amp;")
.replace('"', "&quot;")
.replace('\'', "&#039;")
.replace('<', "&lt;")
.replace('>', "&gt;");
}
[[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 {
<html)"_q
+ classAttribute
+ R"( style=")"
+ EscapeForAttribute(ComputeStyles())
+ Ui::EscapeForAttribute(ComputeStyles())
+ R"(">
<head>
<meta charset="utf-8">
@ -194,7 +133,7 @@ Controller::Controller(
Fn<ShareBoxResult(ShareBoxDescriptor)> 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())
+ "');");
}

View file

@ -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

View file

@ -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> 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!"));

View file

@ -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 {

View file

@ -14,5 +14,10 @@ namespace Platform {
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
callback({});
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
Fn<void(Core::GeoAddress)> callback) {
callback({});
}
} // namespace Platform

View file

@ -116,4 +116,10 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
[[LocationDelegate alloc] initWithCallback:std::move(callback)];
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
Fn<void(Core::GeoAddress)> callback) {
callback({});
}
} // namespace Platform

View file

@ -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<void(Core::GeoLocation)> callback);
void ResolveLocationAddress(
const Core::GeoLocation &location,
Fn<void(Core::GeoAddress)> callback);
} // namespace Platform

View file

@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <winrt/Windows.Devices.Geolocation.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Services.Maps.h>
#include <winrt/Windows.Foundation.Collections.h>
namespace Platform {
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
@ -56,4 +59,10 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
}
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
Fn<void(Core::GeoAddress)> callback) {
callback({});
}
} // namespace Platform

View file

@ -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<VenueRowDelegate*> delegate, const VenueData &data);
void update(const VenueData &data);
[[nodiscard]] VenueData data() const;
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
private:
const not_null<VenueRowDelegate*> _delegate;
VenueData _data;
};
VenueRow::VenueRow(
not_null<VenueRowDelegate*> delegate,
const VenueData &data)
: PeerListRow(UniqueRowIdFromString(data.id))
, _delegate(delegate)
, _data(data) {
setCustomStatus(data.address);
}
void VenueRow::update(const VenueData &data) {
_data = data;
setCustomStatus(data.address);
}
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<Main::Session*> session,
rpl::producer<std::vector<VenueData>> content);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
void rowPaintIcon(
QPainter &p,
int x,
int y,
int size,
const QString &type) override;
private:
struct VenueIcon {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> media;
uint32 paletteVersion : 31 = 0;
uint32 iconLoaded : 1 = 0;
QImage image;
QImage icon;
};
void appendRow(const VenueData &data);
void rebuild(const std::vector<VenueData> &rows);
const not_null<Main::Session*> _session;
rpl::variable<std::vector<VenueData>> _rows;
base::flat_map<QString, VenueIcon> _icons;
rpl::lifetime _lifetime;
};
[[nodiscard]] QString NormalizeVenuesQuery(QString query) {
return query.trimmed().toLower();
}
LinksController::LinksController(
not_null<Main::Session*> session,
rpl::producer<std::vector<VenueData>> content)
: _session(session)
, _rows(std::move(content)) {
}
void LinksController::prepare() {
_rows.value(
) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
rebuild(rows);
}, _lifetime);
}
void LinksController::rebuild(const std::vector<VenueData> &rows) {
auto i = 0;
auto count = delegate()->peerListFullRowsCount();
while (i < rows.size()) {
if (i < count) {
const auto row = delegate()->peerListRowAt(i);
static_cast<VenueRow*>(row.get())->update(rows[i]);
} else {
appendRow(rows[i]);
}
++i;
}
while (i < count) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(i));
--count;
}
delegate()->peerListRefreshRows();
}
void LinksController::rowClicked(not_null<PeerListRow*> row) {
const auto venue = static_cast<VenueRow*>(row.get())->data();
venue;
}
void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true);
}
Main::Session &LinksController::session() const {
return *_session;
}
void LinksController::appendRow(const VenueData &data) {
delegate()->peerListAppendRow(std::make_unique<VenueRow>(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('&', "&amp;")
.replace('"', "&quot;")
.replace('\'', "&#039;")
.replace('<', "&lt;")
.replace('>', "&gt;");
}
[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) {
return value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\'', "\\\'");
static const auto map = base::flat_map<QByteArray, const style::color*>{
{ "window-bg", &st::windowBg },
{ "window-bg-over", &st::windowBgOver },
{ "window-bg-ripple", &st::windowBgRipple },
{ "window-active-text-fg", &st::windowActiveTextFg },
};
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
{ "maps-places-in-area", tr::lng_maps_places_in_area },
};
return Ui::ComputeStyles(map, phrases, Window::Theme::IsNightMode());
}
[[nodiscard]] QByteArray ReadResource(const QString &name) {
@ -115,6 +363,129 @@ QString MapsProviderToken;
)"_q;
}
[[nodiscard]] object_ptr<AbstractButton> MakeSendLocationButton(
QWidget *parent,
rpl::producer<QString> address) {
auto result = object_ptr<FlatButton>(
parent,
QString(),
st::pickLocationButton);
const auto raw = result.data();
const auto st = &st::pickLocationVenue;
const auto icon = CreateChild<RpWidget>(raw);
icon->setGeometry(
st->photoPosition.x(),
st->photoPosition.y(),
st->photoSize,
st->photoSize);
icon->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(icon);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgActive);
p.drawEllipse(icon->rect());
st::pickLocationSendIcon.paintInCenter(p, icon->rect());
}, icon->lifetime());
icon->show();
const auto hadAddress = std::make_shared<bool>(false);
auto statusText = std::move(
address
) | rpl::map([=](const QString &text) {
if (!text.isEmpty()) {
*hadAddress = true;
return text;
}
return *hadAddress ? tr::lng_contacts_loading(tr::now) : QString();
});
const auto name = CreateChild<FlatLabel>(
raw,
tr::lng_maps_point_send(tr::now),
st::pickLocationButtonText);
name->show();
const auto status = CreateChild<FlatLabel>(
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<VerticalLayout*> container,
std::shared_ptr<Main::SessionShow> show,
rpl::producer<std::vector<VenueData>> value) {
auto &lifetime = container->lifetime();
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
show);
const auto controller = lifetime.make_state<LinksController>(
&show->session(),
std::move(value));
controller->setStyleOverrides(&st::pickLocationVenueList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
show->session().downloaderTaskFinished() | rpl::start_with_next([=] {
content->update();
}, content->lifetime());
}
[[nodiscard]] PickerVenueList ParseVenues(
not_null<Main::Session*> session,
const MTPmessages_BotResults &venues) {
const auto &data = venues.data();
session->data().processUsers(data.vusers());
auto &list = data.vresults().v;
auto result = PickerVenueList();
result.list.reserve(list.size());
for (const auto &found : list) {
found.match([&](const auto &data) {
data.vsend_message().match([&](
const MTPDbotInlineMessageMediaVenue &data) {
data.vgeo().match([&](const MTPDgeoPoint &geo) {
result.list.push_back({
.lat = geo.vlat().v,
.lon = geo.vlong().v,
.title = qs(data.vtitle()),
.address = qs(data.vaddress()),
.provider = qs(data.vprovider()),
.id = qs(data.vvenue_id()),
.venueType = qs(data.vvenue_type()),
});
}, [](const auto &) {});
}, [](const auto &) {});
});
}
return result;
}
} // 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<Main::SessionShow> 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<RpWidget>(_body.get());
const auto scroll = CreateChild<ScrollArea>(_body.get());
const auto controls = scroll->setOwnedWidget(
object_ptr<VerticalLayout>(scroll));
_scroll = CreateChild<ScrollArea>(_body.get());
const auto controls = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(_scroll));
const auto toppad = controls->add(object_ptr<RpWidget>(controls));
const auto button = controls->add(object_ptr<FlatButton>(
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<RpWidget>(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<PickerVenueList>(state);
}) | rpl::map([=](PickerVenueState &&state) {
return std::move(v::get<PickerVenueList>(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

View file

@ -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<Data::InputVenue> 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<Main::Session*> session;
Fn<void(LocationInfo)> callback;
Fn<void()> 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<LocationPicker*> 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<Main::SessionShow> 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<SeparatePanel> _window;
not_null<RpWidget*> _body;
RpWidget *_container = nullptr;
ScrollArea *_scroll = nullptr;
std::unique_ptr<Webview::Window> _webview;
SingleQueuedInvokation _updateStyles;
bool _subscribedToColors = false;
Core::GeoLocation _geocoderResolvingFor;
rpl::variable<QString> _geocoderAddress;
rpl::variable<PickerVenueState> _venueState;
const not_null<Main::Session*> _session;
MTP::Sender _api;
UserData *_venuesBot = nullptr;
mtpRequestId _venuesBotRequestId = 0;
mtpRequestId _venuesRequestId = 0;
Core::GeoLocation _venuesRequestLocation;
QString _venuesRequestQuery;
base::flat_map<QString, std::vector<VenuesCacheEntry>> _venuesCache;
};
} // namespace Ui

View file

@ -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<QByteArray, const style::color*> &colors,
const base::flat_map<QByteArray, tr::phrase<>> &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('&', "&amp;")
.replace('"', "&quot;")
.replace('\'', "&#039;")
.replace('<', "&lt;")
.replace('>', "&gt;");
}
QByteArray EscapeForScriptString(QByteArray value) {
return value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\'', "\\\'");
}
} // namespace Ui

View file

@ -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 <typename ...Tags>
struct phrase;
} // namespace tr
namespace Ui {
[[nodiscard]] QByteArray ComputeStyles(
const base::flat_map<QByteArray, const style::color*> &colors,
const base::flat_map<QByteArray, tr::phrase<>> &phrases,
bool nightTheme = false);
[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value);
[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value);
} // namespace Ui

View file

@ -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