diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 10b47447d5587d..f15f85198149e9 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -167,6 +167,8 @@ PRIVATE api/api_user_privacy.h api/api_views.cpp api/api_views.h + api/api_websites.cpp + api/api_websites.h api/api_who_reacted.cpp api/api_who_reacted.h boxes/filters/edit_filter_box.cpp @@ -1270,6 +1272,8 @@ PRIVATE settings/settings_scale_preview.cpp settings/settings_scale_preview.h settings/settings_type.h + settings/settings_websites.cpp + settings/settings_websites.h storage/details/storage_file_utilities.cpp storage/details/storage_file_utilities.h storage/details/storage_settings_scheme.cpp diff --git a/Telegram/Resources/icons/emoji/emoji_skin.png b/Telegram/Resources/icons/emoji/emoji_skin.png new file mode 100644 index 00000000000000..776617ebe0876c Binary files /dev/null and b/Telegram/Resources/icons/emoji/emoji_skin.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_skin@2x.png b/Telegram/Resources/icons/emoji/emoji_skin@2x.png new file mode 100644 index 00000000000000..a53f5c18e7f74a Binary files /dev/null and b/Telegram/Resources/icons/emoji/emoji_skin@2x.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_skin@3x.png b/Telegram/Resources/icons/emoji/emoji_skin@3x.png new file mode 100644 index 00000000000000..fd756d0a9f8377 Binary files /dev/null and b/Telegram/Resources/icons/emoji/emoji_skin@3x.png differ diff --git a/Telegram/Resources/icons/menu/ip_address.png b/Telegram/Resources/icons/menu/ip_address.png new file mode 100644 index 00000000000000..3aa87b0aa5c29c Binary files /dev/null and b/Telegram/Resources/icons/menu/ip_address.png differ diff --git a/Telegram/Resources/icons/menu/ip_address@2x.png b/Telegram/Resources/icons/menu/ip_address@2x.png new file mode 100644 index 00000000000000..1184e10b059654 Binary files /dev/null and b/Telegram/Resources/icons/menu/ip_address@2x.png differ diff --git a/Telegram/Resources/icons/menu/ip_address@3x.png b/Telegram/Resources/icons/menu/ip_address@3x.png new file mode 100644 index 00000000000000..3cee2b09df2b5f Binary files /dev/null and b/Telegram/Resources/icons/menu/ip_address@3x.png differ diff --git a/Telegram/Resources/icons/menu/payment_address.png b/Telegram/Resources/icons/menu/payment_address.png new file mode 100644 index 00000000000000..a7cc0eb69f039d Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_address.png differ diff --git a/Telegram/Resources/icons/menu/payment_address@2x.png b/Telegram/Resources/icons/menu/payment_address@2x.png new file mode 100644 index 00000000000000..12daa492c65894 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_address@2x.png differ diff --git a/Telegram/Resources/icons/menu/payment_address@3x.png b/Telegram/Resources/icons/menu/payment_address@3x.png new file mode 100644 index 00000000000000..3172912c685a79 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_address@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2703e2c711df04..2ac849314f0292 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -646,13 +646,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices."; "lng_settings_security_bots" = "Bots and websites"; "lng_settings_clear_payment_info" = "Clear Payment and Shipping Info"; -"lng_settings_logged_in" = "Logged In with Telegram"; -"lng_settings_logged_in_about" = "Websites where you've used Telegram to log in."; -"lng_settings_logged_in_title" = "Logged In with Telegram"; +"lng_settings_logged_in" = "Connected websites"; +"lng_settings_logged_in_title" = "Logged in with Telegram"; "lng_settings_logged_in_description" = "You can log in on websites that support signing in with Telegram."; -"lng_settings_disconnect_all" = "Disconnect All Websites"; +"lng_settings_disconnect_all" = "Disconnect all websites"; +"lng_settings_disconnect_title" = "Disconnect website"; +"lng_settings_disconnect_sure" = "Are you sure you want to disconnect {domain}?"; +"lng_settings_disconnect_block" = "Block {name}"; +"lng_settings_disconnect_all_title" = "Disconnect websites"; +"lng_settings_disconnect_all_sure" = "Are you sure you want to disconnect all websites where you logged in with Telegram?"; +"lng_settings_disconnect" = "Disconnect"; "lng_settings_connected_title" = "Connected websites"; -"lng_settings_connected_about" = "Click to disconnect from your Telegram account."; "lng_settings_power_menu" = "Battery and Animations"; "lng_settings_power_title" = "Power Usage"; @@ -962,6 +966,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sessions_terminate" = "Terminate Session"; "lng_sessions_application" = "Application"; "lng_sessions_system" = "System version"; +"lng_sessions_browser" = "Browser"; "lng_sessions_ip" = "IP address"; "lng_sessions_location" = "Location"; "lng_sessions_location_about" = "This location is based only on the IP address and may not always be accurate."; @@ -1807,6 +1812,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_emoji_set_active" = "Current set"; "lng_emoji_set_download" = "Download {size}"; "lng_emoji_set_loading" = "{percent}, {progress}"; +"lng_emoji_color_all" = "Choose color for all emoji"; "lng_recent_stickers" = "Frequently used"; "lng_faved_stickers_add" = "Add to Favorites"; diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index 6d82a1d111b395..78e463c116fa64 100644 --- a/Telegram/SourceFiles/api/api_authorizations.cpp +++ b/Telegram/SourceFiles/api/api_authorizations.cpp @@ -72,26 +72,9 @@ Authorizations::Entry ParseEntry(const MTPDauthorization &data) { appName, appVer.isEmpty() ? QString() : (' ' + appVer)); result.ip = qs(data.vip()); - if (!result.hash) { - result.active = tr::lng_status_online(tr::now); - } else { - const auto now = QDateTime::currentDateTime(); - const auto lastTime = base::unixtime::parse(result.activeTime); - const auto nowDate = now.date(); - const auto lastDate = lastTime.date(); - if (lastDate == nowDate) { - result.active = QLocale().toString( - lastTime.time(), - QLocale::ShortFormat); - } else if (lastDate.year() == nowDate.year() - && lastDate.weekNumber() == nowDate.weekNumber()) { - result.active = langDayOfWeek(lastDate); - } else { - result.active = QLocale().toString( - lastDate, - QLocale::ShortFormat); - } - } + result.active = result.hash + ? Authorizations::ActiveDateString(result.activeTime) + : tr::lng_status_online(tr::now); result.location = country; return result; @@ -129,16 +112,15 @@ void Authorizations::reload() { )).done([=](const MTPaccount_Authorizations &result) { _requestId = 0; _lastReceived = crl::now(); - result.match([&](const MTPDaccount_authorizations &auths) { - _ttlDays = auths.vauthorization_ttl_days().v; - _list = ( - auths.vauthorizations().v - ) | ranges::views::transform([](const MTPAuthorization &d) { - return ParseEntry(d.c_authorization()); - }) | ranges::to; - refreshCallsDisabledHereFromCloud(); - _listChanges.fire({}); - }); + const auto &data = result.data(); + _ttlDays = data.vauthorization_ttl_days().v; + _list = ranges::views::all( + data.vauthorizations().v + ) | ranges::views::transform([](const MTPAuthorization &auth) { + return ParseEntry(auth.data()); + }) | ranges::to; + refreshCallsDisabledHereFromCloud(); + _listChanges.fire({}); }).fail([=] { _requestId = 0; }).send(); @@ -190,19 +172,21 @@ Authorizations::List Authorizations::list() const { return _list; } -auto Authorizations::listChanges() const +auto Authorizations::listValue() const -> rpl::producer { return rpl::single( list() ) | rpl::then( - _listChanges.events() | rpl::map([=] { return list(); })); + _listChanges.events() | rpl::map([=] { return list(); }) + ); } -rpl::producer Authorizations::totalChanges() const { +rpl::producer Authorizations::totalValue() const { return rpl::single( total() ) | rpl::then( - _listChanges.events() | rpl::map([=] { return total(); })); + _listChanges.events() | rpl::map([=] { return total(); }) + ); } void Authorizations::updateTTL(int days) { @@ -254,6 +238,19 @@ rpl::producer Authorizations::callsDisabledHereChanges() const { return _callsDisabledHere.changes(); } +QString Authorizations::ActiveDateString(TimeId active) { + const auto now = QDateTime::currentDateTime(); + const auto lastTime = base::unixtime::parse(active); + const auto nowDate = now.date(); + const auto lastDate = lastTime.date(); + return (lastDate == nowDate) + ? QLocale().toString(lastTime.time(), QLocale::ShortFormat) + : (lastDate.year() == nowDate.year() + && lastDate.weekNumber() == nowDate.weekNumber()) + ? langDayOfWeek(lastDate) + : QLocale().toString(lastDate, QLocale::ShortFormat); +} + int Authorizations::total() const { return ranges::count_if( _list, diff --git a/Telegram/SourceFiles/api/api_authorizations.h b/Telegram/SourceFiles/api/api_authorizations.h index 96819edf1456b1..5e2a41c9f90193 100644 --- a/Telegram/SourceFiles/api/api_authorizations.h +++ b/Telegram/SourceFiles/api/api_authorizations.h @@ -38,9 +38,9 @@ class Authorizations final { [[nodiscard]] crl::time lastReceivedTime(); [[nodiscard]] List list() const; - [[nodiscard]] rpl::producer listChanges() const; + [[nodiscard]] rpl::producer listValue() const; [[nodiscard]] int total() const; - [[nodiscard]] rpl::producer totalChanges() const; + [[nodiscard]] rpl::producer totalValue() const; void updateTTL(int days); [[nodiscard]] rpl::producer ttlDays() const; @@ -53,6 +53,8 @@ class Authorizations final { [[nodiscard]] rpl::producer callsDisabledHereValue() const; [[nodiscard]] rpl::producer callsDisabledHereChanges() const; + [[nodiscard]] static QString ActiveDateString(TimeId active); + private: void refreshCallsDisabledHereFromCloud(); diff --git a/Telegram/SourceFiles/api/api_websites.cpp b/Telegram/SourceFiles/api/api_websites.cpp new file mode 100644 index 00000000000000..855056675f5237 --- /dev/null +++ b/Telegram/SourceFiles/api/api_websites.cpp @@ -0,0 +1,138 @@ +/* +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 "api/api_websites.h" + +#include "api/api_authorizations.h" +#include "api/api_blocked_peers.h" +#include "apiwrap.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" + +namespace Api { +namespace { + +constexpr auto TestApiId = 17349; +constexpr auto SnapApiId = 611335; +constexpr auto DesktopApiId = 2040; + +Websites::Entry ParseEntry( + not_null owner, + const MTPDwebAuthorization &data) { + auto result = Websites::Entry{ + .hash = data.vhash().v, + .bot = owner->user(data.vbot_id()), + .platform = qs(data.vplatform()), + .domain = qs(data.vdomain()), + .browser = qs(data.vbrowser()), + .ip = qs(data.vip()), + .location = qs(data.vregion()), + }; + result.activeTime = data.vdate_active().v + ? data.vdate_active().v + : data.vdate_created().v; + result.active = Authorizations::ActiveDateString(result.activeTime); + return result; +} + +} // namespace + +Websites::Websites(not_null api) +: _session(&api->session()) +, _api(&api->instance()) { +} + +void Websites::reload() { + if (_requestId) { + return; + } + + _requestId = _api.request(MTPaccount_GetWebAuthorizations( + )).done([=](const MTPaccount_WebAuthorizations &result) { + _requestId = 0; + _lastReceived = crl::now(); + const auto owner = &_session->data(); + const auto &data = result.data(); + owner->processUsers(data.vusers()); + _list = ranges::views::all( + data.vauthorizations().v + ) | ranges::views::transform([&](const MTPwebAuthorization &auth) { + return ParseEntry(owner, auth.data()); + }) | ranges::to; + _listChanges.fire({}); + }).fail([=] { + _requestId = 0; + }).send(); +} + +void Websites::cancelCurrentRequest() { + _api.request(base::take(_requestId)).cancel(); +} + +void Websites::requestTerminate( + Fn &&done, + Fn &&fail, + std::optional hash, + UserData *botToBlock) { + const auto send = [&](auto request) { + _api.request( + std::move(request) + ).done([=, done = std::move(done)](const MTPBool &result) { + done(result); + if (hash) { + _list.erase( + ranges::remove(_list, *hash, &Entry::hash), + end(_list)); + } else { + _list.clear(); + } + _listChanges.fire({}); + }).fail( + std::move(fail) + ).send(); + }; + if (hash) { + send(MTPaccount_ResetWebAuthorization(MTP_long(*hash))); + if (botToBlock) { + botToBlock->session().api().blockedPeers().block(botToBlock); + } + } else { + send(MTPaccount_ResetWebAuthorizations()); + } +} + +Websites::List Websites::list() const { + return _list; +} + +auto Websites::listValue() const +-> rpl::producer { + return rpl::single( + list() + ) | rpl::then( + _listChanges.events() | rpl::map([=] { return list(); }) + ); +} + +rpl::producer Websites::totalValue() const { + return rpl::single( + total() + ) | rpl::then( + _listChanges.events() | rpl::map([=] { return total(); }) + ); +} + +int Websites::total() const { + return _list.size(); +} + +crl::time Websites::lastReceivedTime() { + return _lastReceived; +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_websites.h b/Telegram/SourceFiles/api/api_websites.h new file mode 100644 index 00000000000000..1551ae4d4ed0b5 --- /dev/null +++ b/Telegram/SourceFiles/api/api_websites.h @@ -0,0 +1,62 @@ +/* +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 "mtproto/sender.h" + +class ApiWrap; + +namespace Main { +class Session; +} // namespace Main + +namespace Api { + +class Websites final { +public: + explicit Websites(not_null api); + + struct Entry { + uint64 hash = 0; + + not_null bot; + TimeId activeTime = 0; + QString active, platform, domain, browser, ip, location; + }; + using List = std::vector; + + void reload(); + void cancelCurrentRequest(); + void requestTerminate( + Fn &&done, + Fn &&fail, + std::optional hash = std::nullopt, + UserData *botToBlock = nullptr); + + [[nodiscard]] crl::time lastReceivedTime(); + + [[nodiscard]] List list() const; + [[nodiscard]] rpl::producer listValue() const; + [[nodiscard]] int total() const; + [[nodiscard]] rpl::producer totalValue() const; + +private: + not_null _session; + + MTP::Sender _api; + mtpRequestId _requestId = 0; + + List _list; + rpl::event_stream<> _listChanges; + + crl::time _lastReceived = 0; + rpl::lifetime _lifetime; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 5ee4847781b5e3..e71e0395304966 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -31,6 +31,7 @@ For license and copyright information please follow this link: #include "api/api_transcribes.h" #include "api/api_premium.h" #include "api/api_user_names.h" +#include "api/api_websites.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -176,7 +177,8 @@ ApiWrap::ApiWrap(not_null session) , _ringtones(std::make_unique(this)) , _transcribes(std::make_unique(this)) , _premium(std::make_unique(this)) -, _usernames(std::make_unique(this)) { +, _usernames(std::make_unique(this)) +, _websites(std::make_unique(this)) { crl::on_main(session, [=] { // You can't use _session->lifetime() in the constructor, // only queued, because it is not constructed yet. @@ -4290,3 +4292,7 @@ Api::Premium &ApiWrap::premium() { Api::Usernames &ApiWrap::usernames() { return *_usernames; } + +Api::Websites &ApiWrap::websites() { + return *_websites; +} diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 5815fd3c0e7019..954500f7660114 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -80,6 +80,7 @@ class Ringtones; class Transcribes; class Premium; class Usernames; +class Websites; namespace details { @@ -383,6 +384,7 @@ class ApiWrap final : public MTP::Sender { [[nodiscard]] Api::Transcribes &transcribes(); [[nodiscard]] Api::Premium &premium(); [[nodiscard]] Api::Usernames &usernames(); + [[nodiscard]] Api::Websites &websites(); void updatePrivacyLastSeens(); @@ -693,6 +695,7 @@ class ApiWrap final : public MTP::Sender { const std::unique_ptr _transcribes; const std::unique_ptr _premium; const std::unique_ptr _usernames; + const std::unique_ptr _websites; mtpRequestId _wallPaperRequestId = 0; QString _wallPaperSlug; diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index c9779b1f15c026..90a989ce714f75 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -517,7 +517,8 @@ void EditCaptionBox::setInitialText() { _field->setTextCursor(cursor); _checkChangedTimer.setCallback([=] { - if (_field->getTextWithAppliedMarkdown() == _initialText) { + if (_field->getTextWithAppliedMarkdown() == _initialText + && _preparedList.files.empty()) { setCloseByOutsideClick(true); } }); @@ -738,6 +739,7 @@ bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) { const auto wasSpoiler = hasSpoiler(); _preparedList = std::move(list); _preparedList.files.front().spoiler = wasSpoiler; + setCloseByOutsideClick(false); rebuildPreview(); return true; } diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index 55b5f499c380a8..5a45ca0de50430 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.cpp +++ b/Telegram/SourceFiles/boxes/sessions_box.cpp @@ -35,10 +35,11 @@ For license and copyright information please follow this link: #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_settings.h" +#include "styles/style_menu_icons.h" namespace { -constexpr auto kSessionsShortPollTimeout = 60 * crl::time(1000); +constexpr auto kShortPollTimeout = 60 * crl::time(1000); constexpr auto kMaxDeviceModelLength = 32; using EntryData = Api::Authorizations::Entry; @@ -80,6 +81,14 @@ class Row final : public PeerListRow { PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; + QSize rightActionSize() const override { + return elementGeometry(2, 0).size(); + } + QMargins rightActionMargins() const override { + const auto rect = elementGeometry(2, 0); + return QMargins(0, rect.y(), -(rect.x() + rect.width()), 0); + } + int elementsCount() const override; QRect elementGeometry(int element, int outerWidth) const override; bool elementDisabled(int element) const override; @@ -458,28 +467,27 @@ void SessionInfoBox( AddSkip(container, st::sessionSubtitleSkip); AddSubsectionTitle(container, tr::lng_sessions_info()); - const auto add = [&](rpl::producer label, QString value) { - if (value.isEmpty()) { - return; - } - container->add( - object_ptr( - container, - rpl::single(value), - st::boxLabel), - st::boxRowPadding + st::sessionValuePadding); - container->add( - object_ptr( - container, - std::move(label), - st::sessionValueLabel), - (st::boxRowPadding - + style::margins{ 0, 0, 0, st::sessionValueSkip })); - }; - add(tr::lng_sessions_application(), data.info); - add(tr::lng_sessions_system(), data.system); - add(tr::lng_sessions_ip(), data.ip); - add(tr::lng_sessions_location(), data.location); + AddSessionInfoRow( + container, + tr::lng_sessions_application(), + data.info, + st::menuIconDevices); + AddSessionInfoRow( + container, + tr::lng_sessions_system(), + data.system, + st::menuIconInfo); + AddSessionInfoRow( + container, + tr::lng_sessions_ip(), + data.ip, + st::menuIconIpAddress); + AddSessionInfoRow( + container, + tr::lng_sessions_location(), + data.location, + st::menuIconAddress); + AddSkip(container, st::sessionValueSkip); if (!data.location.isEmpty()) { AddDividerText(container, tr::lng_sessions_location_about()); @@ -615,8 +623,6 @@ void Row::elementsPaint( outerWidth); } -} // namespace - class SessionsContent : public Ui::RpWidget { public: SessionsContent( @@ -760,7 +766,7 @@ void SessionsContent::setupContent() { _inner->setVisible(!value); }, lifetime()); - _authorizations->listChanges( + _authorizations->listValue( ) | rpl::start_with_next([=](const Api::Authorizations::List &list) { parse(list); }, lifetime()); @@ -791,7 +797,7 @@ void SessionsContent::parse(const Api::Authorizations::List &list) { _inner->showData(_data); - _shortPollTimer.callOnce(kSessionsShortPollTimeout); + _shortPollTimer.callOnce(kShortPollTimeout); } void SessionsContent::resizeEvent(QResizeEvent *e) { @@ -816,7 +822,7 @@ void SessionsContent::paintEvent(QPaintEvent *e) { } void SessionsContent::shortPollSessions() { - const auto left = kSessionsShortPollTimeout + const auto left = kShortPollTimeout - (crl::now() - _authorizations->lastReceivedTime()); if (left > 0) { parse(_authorizations->list()); @@ -1148,27 +1154,7 @@ auto SessionsContent::ListController::Add( return controller; } -SessionsBox::SessionsBox( - QWidget*, - not_null controller) -: _controller(controller) { -} - -void SessionsBox::prepare() { - setTitle(tr::lng_sessions_other_header()); - - addButton(tr::lng_close(), [=] { closeBox(); }); - - const auto w = st::boxWideWidth; - - const auto content = setInnerWidget( - object_ptr(this, _controller), - st::sessionsScroll); - content->resize(w, st::noContactsHeight); - content->setupContent(); - - setDimensions(w, st::sessionsHeight); -} +} // namespace namespace Settings { @@ -1193,4 +1179,41 @@ void Sessions::setupContent(not_null controller) { Ui::ResizeFitChild(this, container); } +void AddSessionInfoRow( + not_null container, + rpl::producer label, + const QString &value, + const style::icon &icon) { + if (value.isEmpty()) { + return; + } + + const auto text = container->add( + object_ptr( + container, + rpl::single(value), + st::boxLabel), + st::boxRowPadding + st::sessionValuePadding); + const auto left = st::sessionValuePadding.left(); + container->add( + object_ptr( + container, + std::move(label), + st::sessionValueLabel), + (st::boxRowPadding + + style::margins{ left, 0, 0, st::sessionValueSkip })); + + const auto widget = Ui::CreateChild(container.get()); + widget->resize(icon.size()); + + text->topValue() | rpl::start_with_next([=](int top) { + widget->move(st::sessionValueIconPosition + QPoint(0, top)); + }, widget->lifetime()); + + widget->paintRequest() | rpl::start_with_next([=, &icon] { + auto p = QPainter(widget); + icon.paintInCenter(p, widget->rect()); + }, widget->lifetime()); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/boxes/sessions_box.h b/Telegram/SourceFiles/boxes/sessions_box.h index d735189a5d5336..47d03de2713445 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.h +++ b/Telegram/SourceFiles/boxes/sessions_box.h @@ -7,12 +7,11 @@ For license and copyright information please follow this link: */ #pragma once -#include "boxes/abstract_box.h" #include "settings/settings_common.h" -namespace Main { -class Session; -} // namespace Main +namespace Ui { +class VerticalLayout; +} // namespace Ui namespace Settings { @@ -29,16 +28,10 @@ class Sessions : public Section { }; -} // namespace Settings - -class SessionsBox : public Ui::BoxContent { -public: - SessionsBox(QWidget*, not_null controller); - -protected: - void prepare() override; +void AddSessionInfoRow( + not_null container, + rpl::producer label, + const QString &value, + const style::icon &icon); -private: - const not_null _controller; - -}; +} // namespace Settings diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 75fbfbbb020485..fb85201ebeb6b1 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -118,6 +118,8 @@ EmojiPan { tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; + colorAll: IconButton; + colorAllLabel: FlatLabel; removeSet: IconButton; boxLabel: FlatLabel; icons: ComposeIcons; @@ -448,6 +450,7 @@ inlineResultsMaxHeight: 640px; emojiPanHeaderFont: semiboldFont; emojiPanRemoveSkip: 10px; emojiPanRemoveTop: 10px; +emojiPanColorAllSkip: 9px; emojiColorsPadding: 5px; emojiColorsSep: 1px; @@ -490,6 +493,25 @@ stickerIconMove: 400; stickerPreviewDuration: 150; stickerPreviewMin: 0.1; +emojiPanColorAll: IconButton(stickerPanRemoveSet) { + width: 24px; + height: 24px; + rippleAreaSize: 24px; + icon: icon {{ "emoji/emoji_skin", smallCloseIconFg }}; + iconOver: icon {{ "emoji/emoji_skin", smallCloseIconFgOver }}; +} +emojiPanColorAllLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + align: align(top); + minWidth: 40px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px); + linkFontOver: font(12px); + } +} +emojiPanColorAllPadding: margins(10px, 6px, 10px, -1px); + stickerGroupCategorySize: 28px; stickerGroupCategoryAbout: defaultTextStyle; stickerGroupCategoryAddMargin: margins(0px, 10px, 0px, 5px); @@ -626,6 +648,8 @@ defaultEmojiPan: EmojiPan { tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); + colorAll: emojiPanColorAll; + colorAllLabel: emojiPanColorAllLabel; removeSet: stickerPanRemoveSet; boxLabel: boxLabel; icons: defaultComposeIcons; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp index 6840276246d663..500de20b6ccc92 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp @@ -672,17 +672,9 @@ std::vector EmojiKeywords::PrioritizeRecent( } std::vector EmojiKeywords::ApplyVariants(std::vector list) { + auto &settings = Core::App().settings(); for (auto &item : list) { - item.emoji = [&] { - const auto result = item.emoji; - const auto &variants = Core::App().settings().emojiVariants(); - const auto i = result->hasVariants() - ? variants.find(result->nonColoredId()) - : end(variants); - return (i != end(variants)) - ? result->variant(i->second) - : result; - }(); + item.emoji = settings.lookupEmojiVariant(item.emoji); } return list; } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index f41482c190b819..11fa6b2341fedd 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -58,7 +58,7 @@ class EmojiColorPicker final : public Ui::RpWidget { public: EmojiColorPicker(QWidget *parent, const style::EmojiPan &st); - void showEmoji(EmojiPtr emoji); + void showEmoji(EmojiPtr emoji, bool allLabel = false); void clearSelection(); void handleMouseMove(QPoint globalPos); @@ -79,8 +79,10 @@ class EmojiColorPicker final : public Ui::RpWidget { void mouseMoveEvent(QMouseEvent *e) override; private: + void createAllLabel(); void animationCallback(); void updateSize(); + [[nodiscard]] int topColorAllSkip() const; void drawVariant(QPainter &p, int variant); @@ -106,6 +108,8 @@ class EmojiColorPicker final : public Ui::RpWidget { QPixmap _cache; Ui::Animations::Simple _a_opacity; + std::unique_ptr _allLabel; + rpl::event_stream _chosen; rpl::event_stream<> _hidden; @@ -131,10 +135,15 @@ EmojiColorPicker::EmojiColorPicker( setMouseTracking(true); } -void EmojiColorPicker::showEmoji(EmojiPtr emoji) { +void EmojiColorPicker::showEmoji(EmojiPtr emoji, bool allLabel) { if (!emoji || !emoji->hasVariants()) { return; } + if (!allLabel) { + _allLabel = nullptr; + } else if (!_allLabel) { + createAllLabel(); + } _ignoreShow = false; _variants.resize(emoji->variantsCount() + 1); @@ -144,10 +153,21 @@ void EmojiColorPicker::showEmoji(EmojiPtr emoji) { updateSize(); - if (!_cache.isNull()) _cache = QPixmap(); + if (!_cache.isNull()) { + _cache = QPixmap(); + } showAnimated(); } +void EmojiColorPicker::createAllLabel() { + _allLabel = std::make_unique( + this, + tr::lng_emoji_color_all(), + _st.colorAllLabel); + _allLabel->show(); + _allLabel->setAttribute(Qt::WA_TransparentForMouseEvents); +} + void EmojiColorPicker::updateSize() { auto width = st::emojiPanMargins.left() + _singleSize.width() * _variants.size() @@ -158,6 +178,17 @@ void EmojiColorPicker::updateSize() { + 2 * st::emojiColorsPadding + _singleSize.height() + st::emojiPanMargins.bottom(); + if (_allLabel) { + _allLabel->resizeToWidth(width + - st::emojiPanMargins.left() + - st::emojiPanMargins.right() + - st::emojiPanColorAllPadding.left() + - st::emojiPanColorAllPadding.right()); + _allLabel->move( + st::emojiPanMargins.left() + st::emojiPanColorAllPadding.left(), + st::emojiPanMargins.top() + st::emojiPanColorAllPadding.top()); + height += topColorAllSkip(); + } resize(width, height); update(); updateSelected(); @@ -186,11 +217,15 @@ void EmojiColorPicker::paintEvent(QPaintEvent *e) { Ui::Shadow::paint(p, inner, width(), _st.showAnimation.shadow); _backgroundRect.paint(p, inner); + const auto skip = topColorAllSkip(); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; - p.fillRect(x, st::emojiPanMargins.top() + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2, st::emojiColorsSepColor); + p.fillRect(x, st::emojiPanMargins.top() + skip + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2 - skip, st::emojiColorsSepColor); - if (_variants.isEmpty()) return; + if (_variants.isEmpty()) { + return; + } + p.translate(0, skip); for (auto i = 0, count = int(_variants.size()); i != count; ++i) { drawVariant(p, i); } @@ -248,6 +283,9 @@ void EmojiColorPicker::animationCallback() { update(); if (!_a_opacity.animating()) { _cache = QPixmap(); + if (_allLabel) { + _allLabel->show(); + } if (_hiding) { hide(); _hidden.fire({}); @@ -276,10 +314,16 @@ rpl::producer<> EmojiColorPicker::hidden() const { void EmojiColorPicker::hideAnimated() { if (_cache.isNull()) { + if (_allLabel) { + _allLabel->show(); + } _cache = Ui::GrabWidget(this); clearSelection(); } _hiding = true; + if (_allLabel) { + _allLabel->hide(); + } _a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration); } @@ -291,10 +335,16 @@ void EmojiColorPicker::showAnimated() { } _hiding = false; if (_cache.isNull()) { + if (_allLabel) { + _allLabel->show(); + } _cache = Ui::GrabWidget(this); clearSelection(); } show(); + if (_allLabel) { + _allLabel->hide(); + } _a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration); } @@ -304,10 +354,18 @@ void EmojiColorPicker::clearSelection() { _lastMousePos = mapToGlobal(QPoint(-10, -10)); } +int EmojiColorPicker::topColorAllSkip() const { + return _allLabel + ? (st::emojiPanColorAllPadding.top() + + _allLabel->height() + + st::emojiPanColorAllPadding.bottom()) + : 0; +} + void EmojiColorPicker::updateSelected() { auto newSelected = -1; auto p = mapFromGlobal(_lastMousePos); - auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - st::emojiColorsPadding; + auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - topColorAllSkip() - st::emojiColorsPadding; if (y >= 0 && y < _singleSize.height()) { auto x = sx - st::emojiPanMargins.left() - st::emojiColorsPadding; if (x >= 0 && x < _singleSize.width()) { @@ -327,7 +385,8 @@ void EmojiColorPicker::setSelected(int newSelected) { if (_selected == newSelected) { return; } - auto updateSelectedRect = [this] { + const auto skip = topColorAllSkip(); + const auto updateSelectedRect = [&] { if (_selected < 0) return; auto addedSkip = (_selected > 0) ? (2 * st::emojiColorsPadding + st::emojiColorsSep) @@ -338,7 +397,7 @@ void EmojiColorPicker::setSelected(int newSelected) { + addedSkip; rtlupdate( left, - st::emojiPanMargins.top() + st::emojiColorsPadding, + st::emojiPanMargins.top() + st::emojiColorsPadding + skip, _singleSize.width(), _singleSize.height()); }; @@ -851,6 +910,31 @@ void EmojiListWidget::setSingleSize(QSize size) { _picker->setSingleSize(_singleSize); } +void EmojiListWidget::setColorAllForceRippled(bool force) { + _colorAllRippleForced = force; + if (_colorAllRippleForced) { + _colorAllRippleForcedLifetime = style::PaletteChanged( + ) | rpl::filter([=] { + return _colorAllRipple != nullptr; + }) | rpl::start_with_next([=] { + _colorAllRipple->forceRepaint(); + }); + if (!_colorAllRipple) { + _colorAllRipple = createButtonRipple(int(Section::People)); + } + if (_colorAllRipple->empty()) { + _colorAllRipple->addFading(); + } else { + _colorAllRipple->lastUnstop(); + } + } else { + if (_colorAllRipple) { + _colorAllRipple->lastStop(); + } + _colorAllRippleForcedLifetime.destroy(); + } +} + int EmojiListWidget::countDesiredHeight(int newWidth) { const auto fullWidth = st().margin.left() + newWidth @@ -897,14 +981,9 @@ void EmojiListWidget::ensureLoaded(int section) { _emoji[section] = Ui::Emoji::GetSection(static_cast
(section)); _counts[section] = _emoji[section].size(); - const auto &variants = Core::App().settings().emojiVariants(); + const auto &settings = Core::App().settings(); for (auto &emoji : _emoji[section]) { - if (emoji->hasVariants()) { - const auto j = variants.find(emoji->nonColoredId()); - if (j != end(variants)) { - emoji = emoji->variant(j->second); - } - } + emoji = settings.lookupEmojiVariant(emoji); } } @@ -1366,8 +1445,7 @@ void EmojiListWidget::mousePressEvent(QMouseEvent *e) { if (emoji && emoji->hasVariants()) { _pickerSelected = _selected; setCursor(style::cur_default); - const auto &variants = Core::App().settings().emojiVariants(); - if (!variants.contains(emoji->nonColoredId())) { + if (!Core::App().settings().hasChosenEmojiVariant(emoji)) { showPicker(); } else { _showPickerTimer.callOnce(500); @@ -1385,12 +1463,11 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { return _picker->handleMouseRelease(QCursor::pos()); } else if (const auto over = std::get_if(&_pickerSelected)) { const auto emoji = lookupOverEmoji(over); - if (emoji && emoji->hasVariants()) { - const auto &variants = Core::App().settings().emojiVariants(); - if (variants.contains(emoji->nonColoredId())) { - _picker->hideAnimated(); - _pickerSelected = v::null; - } + if (emoji + && emoji->hasVariants() + && Core::App().settings().hasChosenEmojiVariant(emoji)) { + _picker->hideAnimated(); + _pickerSelected = v::null; } } } @@ -1429,11 +1506,15 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { && set->section < _staticCount + _custom.size()); displaySet(_custom[set->section - _staticCount].id); } else if (auto button = std::get_if(&pressed)) { - Assert(button->section >= _staticCount - && button->section < _staticCount + _custom.size()); - const auto id = _custom[button->section - _staticCount].id; + Assert(hasButton(button->section)); + const auto id = hasColorButton(button->section) + ? 0 + : _custom[button->section - _staticCount].id; const auto usage = ChatHelpers::WindowUsage::PremiumPromo; - if (hasRemoveButton(button->section)) { + if (hasColorButton(button->section)) { + _pickerSelected = pressed; + showPicker(); + } else if (hasRemoveButton(button->section)) { removeSet(id); } else if (hasAddButton(button->section)) { _localSetsManager->install(id); @@ -1496,23 +1577,35 @@ void EmojiListWidget::showPicker() { if (v::is_null(_pickerSelected)) { return; } - - const auto over = std::get_if(&_pickerSelected); - const auto emoji = lookupOverEmoji(over); - if (emoji && emoji->hasVariants()) { - _picker->showEmoji(emoji); - - auto y = emojiRect(over->section, over->index).y(); + const auto showAt = [&](float64 xCoef, int y, int height) { y -= _picker->height() - st::emojiPanRadius + getVisibleTop(); if (y < st().header) { - y += _picker->height() - st::emojiPanRadius + _singleSize.height() - st::emojiPanRadius; + y += _picker->height() + height; } auto xmax = width() - _picker->width(); - auto coef = float64(over->index % _columnCount) / float64(_columnCount - 1); - if (rtl()) coef = 1. - coef; - _picker->move(qRound(xmax * coef), y); + if (rtl()) xCoef = 1. - xCoef; + _picker->move(qRound(xmax * xCoef), y); disableScroll(true); + }; + if (const auto button = std::get_if(&_pickerSelected)) { + const auto hand = QString::fromUtf8("\xF0\x9F\x91\x8B"); + const auto emoji = Ui::Emoji::Find(hand); + Assert(emoji != nullptr && emoji->hasVariants()); + _picker->showEmoji(emoji, true); + setColorAllForceRippled(true); + const auto rect = buttonRect(button->section); + showAt(1., rect.y(), rect.height() - 2 * st::emojiPanRadius); + } else if (const auto over = std::get_if(&_pickerSelected)) { + const auto emoji = lookupOverEmoji(over); + if (emoji && emoji->hasVariants()) { + _picker->showEmoji(emoji); + + const auto coef = float64(over->index % _columnCount) + / float64(_columnCount - 1); + const auto h = _singleSize.height() - 2 * st::emojiPanRadius; + showAt(coef, emojiRect(over->section, over->index).y(), h); + } } } @@ -1520,11 +1613,34 @@ void EmojiListWidget::pickerHidden() { _pickerSelected = v::null; update(); disableScroll(false); + setColorAllForceRippled(false); _lastMousePos = QCursor::pos(); updateSelected(); } +bool EmojiListWidget::hasColorButton(int index) const { + return (_staticCount > int(Section::People)) + && (index == int(Section::People)); +} + +QRect EmojiListWidget::colorButtonRect(int index) const { + return colorButtonRect(sectionInfo(index)); +} + +QRect EmojiListWidget::colorButtonRect(const SectionInfo &info) const { + if (_mode != Mode::Full) { + return QRect(); + } + const auto &colorSt = st().colorAll; + const auto buttonw = colorSt.rippleAreaPosition.x() + + colorSt.rippleAreaSize; + const auto buttonh = colorSt.height; + const auto buttonx = emojiRight() - st::emojiPanColorAllSkip - buttonw; + const auto buttony = info.top + st::emojiPanRemoveTop; + return QRect(buttonx, buttony, buttonw, buttonh); +} + bool EmojiListWidget::hasRemoveButton(int index) const { if (index < _staticCount || index >= _staticCount + _custom.size()) { @@ -1581,15 +1697,18 @@ QRect EmojiListWidget::unlockButtonRect(int index) const { } bool EmojiListWidget::hasButton(int index) const { - if (index < _staticCount - || index >= _staticCount + _custom.size()) { - return false; + if (hasColorButton(index) + || (index >= _staticCount + && index < _staticCount + _custom.size())) { + return true; } - return true; + return false; } QRect EmojiListWidget::buttonRect(int index) const { - return hasRemoveButton(index) + return hasColorButton(index) + ? colorButtonRect(index) + : hasRemoveButton(index) ? removeButtonRect(index) : hasAddButton(index) ? addButtonRect(index) @@ -1637,19 +1756,33 @@ QRect EmojiListWidget::emojiRect(int section, int index) const { } void EmojiListWidget::colorChosen(EmojiChosen data) { + Expects(data.emoji != nullptr && data.emoji->hasVariants()); + const auto emoji = data.emoji; - if (emoji->hasVariants()) { - Core::App().settings().saveEmojiVariant(emoji); - } - const auto over = std::get_if(&_pickerSelected); - if (over - && over->section > int(Section::Recent) - && over->section < _staticCount - && over->index < _emoji[over->section].size()) { - _emoji[over->section][over->index] = emoji; - rtlupdate(emojiRect(over->section, over->index)); - } - selectEmoji(data); + auto &settings = Core::App().settings(); + if (const auto button = std::get_if(&_pickerSelected)) { + settings.saveAllEmojiVariants(emoji); + for (auto section = int(Section::People) + ; section < _staticCount + ; ++section) { + for (auto &emoji : _emoji[section]) { + emoji = settings.lookupEmojiVariant(emoji); + } + } + update(); + } else { + settings.saveEmojiVariant(emoji); + + const auto over = std::get_if(&_pickerSelected); + if (over + && over->section > int(Section::Recent) + && over->section < _staticCount + && over->index < _emoji[over->section].size()) { + _emoji[over->section][over->index] = emoji; + rtlupdate(emojiRect(over->section, over->index)); + } + selectEmoji(data); + } _picker->hideAnimated(); } @@ -1967,47 +2100,54 @@ int EmojiListWidget::paintButtonGetWidth( const SectionInfo &info, bool selected, QRect clip) const { - if (info.section < _staticCount - || info.section >= _staticCount + _custom.size()) { + if (!hasButton(info.section)) { return 0; } - auto &custom = _custom[info.section - _staticCount]; - if (hasRemoveButton(info.section)) { - const auto remove = removeButtonRect(info); - if (remove.isEmpty()) { + auto &ripple = (info.section >= _staticCount) + ? _custom[info.section - _staticCount].ripple + : _colorAllRipple; + const auto colorAll = hasColorButton(info.section); + if (colorAll || hasRemoveButton(info.section)) { + const auto rect = colorAll + ? colorButtonRect(info) + : removeButtonRect(info); + if (rect.isEmpty()) { return 0; - } else if (remove.intersects(clip)) { - const auto &removeSt = st().removeSet; - if (custom.ripple) { - custom.ripple->paint( + } else if (rect.intersects(clip)) { + const auto &bst = colorAll ? st().colorAll : st().removeSet; + if (colorAll && _colorAllRippleForced) { + selected = true; + } + if (ripple) { + ripple->paint( p, - remove.x() + removeSt.rippleAreaPosition.x(), - remove.y() + removeSt.rippleAreaPosition.y(), + rect.x() + bst.rippleAreaPosition.x(), + rect.y() + bst.rippleAreaPosition.y(), width()); - if (custom.ripple->empty()) { - custom.ripple.reset(); + if (ripple->empty()) { + ripple.reset(); } } - const auto &icon = selected ? removeSt.iconOver : removeSt.icon; + const auto &icon = selected ? bst.iconOver : bst.icon; icon.paint( p, - (remove.topLeft() + (rect.topLeft() + QPoint( - remove.width() - icon.width(), - remove.height() - icon.height()) / 2), + rect.width() - icon.width(), + rect.height() - icon.height()) / 2), width()); } - return emojiRight() - remove.x(); + return emojiRight() - rect.x(); } const auto canAdd = hasAddButton(info.section); const auto &button = rightButton(info.section); const auto rect = buttonRect(info, button); p.drawImage(rect.topLeft(), selected ? button.backOver : button.back); - if (custom.ripple) { - const auto ripple = QColor(0, 0, 0, 36); - custom.ripple->paint(p, rect.x(), rect.y(), width(), &ripple); - if (custom.ripple->empty()) { - custom.ripple.reset(); + if (ripple) { + const auto color = QColor(0, 0, 0, 36); + ripple->paint(p, rect.x(), rect.y(), width(), &color); + if (ripple->empty()) { + ripple.reset(); } } p.setPen(!canAdd @@ -2108,22 +2248,28 @@ void EmojiListWidget::setSelected(OverState newSelected) { void EmojiListWidget::setPressed(OverState newPressed) { if (auto button = std::get_if(&_pressed)) { - Assert(button->section >= _staticCount - && button->section < _staticCount + _custom.size()); - auto &set = _custom[button->section - _staticCount]; - if (set.ripple) { - set.ripple->lastStop(); + Assert(hasColorButton(button->section) + || (button->section >= _staticCount + && button->section < _staticCount + _custom.size())); + auto &ripple = (button->section >= _staticCount) + ? _custom[button->section - _staticCount].ripple + : _colorAllRipple; + if (ripple) { + ripple->lastStop(); } } _pressed = newPressed; if (auto button = std::get_if(&_pressed)) { - Assert(button->section >= _staticCount - && button->section < _staticCount + _custom.size()); - auto &set = _custom[button->section - _staticCount]; - if (!set.ripple) { - set.ripple = createButtonRipple(button->section); + Assert(hasColorButton(button->section) + || (button->section >= _staticCount + && button->section < _staticCount + _custom.size())); + auto &ripple = (button->section >= _staticCount) + ? _custom[button->section - _staticCount].ripple + : _colorAllRipple; + if (!ripple) { + ripple = createButtonRipple(button->section); } - set.ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); + ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); } } @@ -2167,16 +2313,18 @@ void EmojiListWidget::initButton( std::unique_ptr EmojiListWidget::createButtonRipple( int section) { - Expects(section >= _staticCount - && section < _staticCount + _custom.size()); + Expects(hasButton(section)); + const auto colorAll = hasColorButton(section); const auto remove = hasRemoveButton(section); - const auto &removeSt = st().removeSet; - const auto &st = remove ? removeSt.ripple : st::emojiPanButton.ripple; - auto mask = remove + const auto &staticSt = colorAll ? st().colorAll : st().removeSet; + const auto &st = (colorAll || remove) + ? staticSt.ripple + : st::emojiPanButton.ripple; + auto mask = (colorAll || remove) ? Ui::RippleAnimation::EllipseMask(QSize( - removeSt.rippleAreaSize, - removeSt.rippleAreaSize)) + staticSt.rippleAreaSize, + staticSt.rippleAreaSize)) : rightButton(section).rippleMask; return std::make_unique( st, @@ -2185,11 +2333,12 @@ std::unique_ptr EmojiListWidget::createButtonRipple( } QPoint EmojiListWidget::buttonRippleTopLeft(int section) const { - Expects(section >= _staticCount - && section < _staticCount + _custom.size()); + Expects(hasButton(section)); return myrtlrect(buttonRect(section)).topLeft() - + (hasRemoveButton(section) + + (hasColorButton(section) + ? st().colorAll.rippleAreaPosition + : hasRemoveButton(section) ? st().removeSet.rippleAreaPosition : QPoint()); } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 374b6e7b37f797..b042a568e4923b 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -247,6 +247,7 @@ class EmojiListWidget final [[nodiscard]] SectionInfo sectionInfoByOffset(int yOffset) const; [[nodiscard]] int sectionsCount() const; void setSingleSize(QSize size); + void setColorAllForceRippled(bool force); void showPicker(); void pickerHidden(); @@ -297,6 +298,9 @@ class EmojiListWidget final int set, int index); void validateEmojiPaintContext(const ExpandingContext &context); + [[nodiscard]] bool hasColorButton(int index) const; + [[nodiscard]] QRect colorButtonRect(int index) const; + [[nodiscard]] QRect colorButtonRect(const SectionInfo &info) const; [[nodiscard]] bool hasRemoveButton(int index) const; [[nodiscard]] QRect removeButtonRect(int index) const; [[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const; @@ -378,6 +382,10 @@ class EmojiListWidget final Ui::RoundRect _overBg; QImage _searchExpandCache; + mutable std::unique_ptr _colorAllRipple; + bool _colorAllRippleForced = false; + rpl::lifetime _colorAllRippleForcedLifetime; + std::vector _nextSearchQuery; std::vector _searchQuery; base::flat_set _searchEmoji; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index cef0bd2e8a4797..8abdd7496055e5 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -1076,11 +1076,40 @@ void Settings::setLegacyRecentEmojiPreload( } } +EmojiPtr Settings::lookupEmojiVariant(EmojiPtr emoji) const { + if (emoji->hasVariants()) { + const auto i = _emojiVariants.find(emoji->nonColoredId()); + if (i != end(_emojiVariants)) { + return emoji->variant(i->second); + } + const auto j = _emojiVariants.find(QString()); + if (j != end(_emojiVariants)) { + return emoji->variant(j->second); + } + } + return emoji; +} + +bool Settings::hasChosenEmojiVariant(EmojiPtr emoji) const { + return _emojiVariants.contains(QString()) + || _emojiVariants.contains(emoji->nonColoredId()); +} + void Settings::saveEmojiVariant(EmojiPtr emoji) { + Expects(emoji->hasVariants()); + _emojiVariants[emoji->nonColoredId()] = emoji->variantIndex(emoji); _saveDelayed.fire({}); } +void Settings::saveAllEmojiVariants(EmojiPtr emoji) { + Expects(emoji->hasVariants()); + + _emojiVariants.clear(); + _emojiVariants[QString()] = emoji->variantIndex(emoji); + _saveDelayed.fire({}); +} + void Settings::setLegacyEmojiVariants(QMap data) { if (!_emojiVariants.empty() || data.isEmpty()) { return; diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 72468e7552f249..bc7b7428266385 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -668,7 +668,10 @@ class Settings final { [[nodiscard]] const base::flat_map &emojiVariants() const { return _emojiVariants; } + [[nodiscard]] EmojiPtr lookupEmojiVariant(EmojiPtr emoji) const; + [[nodiscard]] bool hasChosenEmojiVariant(EmojiPtr emoji) const; void saveEmojiVariant(EmojiPtr emoji); + void saveAllEmojiVariants(EmojiPtr emoji); void setLegacyEmojiVariants(QMap data); [[nodiscard]] bool disableOpenGL() const { diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index e7351e8fd9e65f..ead942262ee59a 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -282,15 +282,10 @@ rpl::producer<> UiIntegration::forcePopupMenuHideRequests() { const Ui::Emoji::One *UiIntegration::defaultEmojiVariant( const Ui::Emoji::One *emoji) { - if (!emoji || !emoji->hasVariants()) { + if (!emoji) { return emoji; } - const auto nonColored = emoji->nonColoredId(); - const auto &variants = Core::App().settings().emojiVariants(); - const auto i = variants.find(nonColored); - const auto result = (i != end(variants)) - ? emoji->variant(i->second) - : emoji; + const auto result = Core::App().settings().lookupEmojiVariant(emoji); Core::App().settings().incrementRecentEmoji({ result }); return result; } diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 088dc0b92d8419..c1a0719febe3ae 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -86,6 +86,45 @@ constexpr auto ByDocument = [](const auto &entry) { return nullptr; } +[[nodiscard]] bool ItemContainsMedia(const DownloadObject &object) { + if (const auto photo = object.photo) { + if (const auto media = object.item->media()) { + if (const auto page = media->webpage()) { + if (page->photo == photo) { + return true; + } + for (const auto &item : page->collage.items) { + if (const auto v = std::get_if(&item)) { + if ((*v) == photo) { + return true; + } + } + } + } else { + return (media->photo() == photo); + } + } + } else if (const auto document = object.document) { + if (const auto media = object.item->media()) { + if (const auto page = media->webpage()) { + if (page->document == document) { + return true; + } + for (const auto &item : page->collage.items) { + if (const auto v = std::get_if(&item)) { + if ((*v) == document) { + return true; + } + } + } + } else { + return (media->document() == document); + } + } + } + return false; +} + struct DocumentDescriptor { uint64 sessionUniqueId = 0; DocumentId documentId = 0; @@ -242,12 +281,12 @@ void DownloadManager::check( std::vector::iterator i) { auto &entry = *i; - const auto photo = ItemPhoto(entry.object.item); - const auto document = ItemDocument(entry.object.item); - if (entry.object.photo != photo || entry.object.document != document) { + if (!ItemContainsMedia(entry.object)) { cancel(data, i); return; } + const auto document = entry.object.document; + // Load with progress only documents for now. Assert(document != nullptr); diff --git a/Telegram/SourceFiles/data/data_file_click_handler.cpp b/Telegram/SourceFiles/data/data_file_click_handler.cpp index ec746f5667daea..e42397d197e112 100644 --- a/Telegram/SourceFiles/data/data_file_click_handler.cpp +++ b/Telegram/SourceFiles/data/data_file_click_handler.cpp @@ -74,7 +74,8 @@ void DocumentOpenClickHandler::onClickImpl() const { void DocumentSaveClickHandler::Save( Data::FileOrigin origin, not_null data, - Mode mode) { + Mode mode, + Fn started) { if (data->isNull()) { return; } @@ -89,6 +90,9 @@ void DocumentSaveClickHandler::Save( // background thread timers from working which would // stop audio playback in voice chats / live streams. if (mode != Mode::ToNewFile && data->saveFromData()) { + if (started) { + started(); + } return; } const auto filepath = data->filepath(true); @@ -107,6 +111,9 @@ void DocumentSaveClickHandler::Save( filedir); if (!savename.isEmpty()) { data->save(origin, savename); + if (started) { + started(); + } } })); } @@ -114,16 +121,21 @@ void DocumentSaveClickHandler::Save( void DocumentSaveClickHandler::SaveAndTrack( FullMsgId itemId, not_null document, - Mode mode) { - Save(itemId ? itemId : Data::FileOrigin(), document, mode); - if (document->loading() && !document->loadingFilePath().isEmpty()) { - if (const auto item = document->owner().message(itemId)) { - Core::App().downloadManager().addLoading({ - .item = item, - .document = document, - }); + Mode mode, + Fn started) { + Save(itemId ? itemId : Data::FileOrigin(), document, mode, [=] { + if (document->loading() && !document->loadingFilePath().isEmpty()) { + if (const auto item = document->owner().message(itemId)) { + Core::App().downloadManager().addLoading({ + .item = item, + .document = document, + }); + } } - } + if (started) { + started(); + } + }); } void DocumentSaveClickHandler::onClickImpl() const { diff --git a/Telegram/SourceFiles/data/data_file_click_handler.h b/Telegram/SourceFiles/data/data_file_click_handler.h index 472d04d15e0ada..46eaa5b824aefe 100644 --- a/Telegram/SourceFiles/data/data_file_click_handler.h +++ b/Telegram/SourceFiles/data/data_file_click_handler.h @@ -53,11 +53,13 @@ class DocumentSaveClickHandler : public DocumentClickHandler { static void Save( Data::FileOrigin origin, not_null document, - Mode mode = Mode::ToCacheOrFile); + Mode mode = Mode::ToCacheOrFile, + Fn started = nullptr); static void SaveAndTrack( FullMsgId itemId, not_null document, - Mode mode = Mode::ToCacheOrFile); + Mode mode = Mode::ToCacheOrFile, + Fn started = nullptr); protected: void onClickImpl() const override; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index a2426a990a546a..67e68476491414 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -479,6 +479,14 @@ storiesRemoveSet: IconButton(stickerPanRemoveSet) { iconOver: icon {{ "simple_close", storiesComposeGrayIcon }}; ripple: storiesComposeRippleLight; } +storiesColorAll: IconButton(emojiPanColorAll) { + icon: icon {{ "emoji/emoji_skin", storiesComposeGrayIcon }}; + iconOver: icon {{ "emoji/emoji_skin", storiesComposeGrayIcon }}; + ripple: storiesComposeRippleLight; +} +storiesColorAllLabel: FlatLabel(emojiPanColorAllLabel) { + textFg: storiesComposeGrayText; +} storiesMenuSeparator: mediaviewMenuSeparator; storiesMenu: Menu(defaultMenu) { itemBg: groupCallMenuBg; @@ -588,6 +596,8 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) { rippleBgActive: storiesComposeBgOver; } search: storiesEmojiTabbedSearch; + colorAll: storiesColorAll; + colorAllLabel: storiesColorAllLabel; removeSet: storiesRemoveSet; boxLabel: storiesBoxLabel; icons: ComposeIcons { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 0a58cdb5e5e361..957679abab48e3 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -2055,7 +2055,7 @@ void OverlayWidget::zoomOut() { } void OverlayWidget::zoomReset() { - if (_stories) { + if (_stories || _fullScreenVideo) { return; } auto newZoom = _zoom; @@ -2512,19 +2512,26 @@ void OverlayWidget::downloadMedia() { } else { if (_document->filepath(true).isEmpty() && !_document->loading()) { + const auto document = _document; + const auto checkSaveStarted = [=] { + if (isHidden() || _document != document) { + return; + } + _documentLoadingTo = _document->loadingFilePath(); + if (_stories && _documentLoadingTo.isEmpty()) { + const auto toName = _document->filepath(true); + if (!toName.isEmpty()) { + showSaveMsgToast( + toName, + tr::lng_mediaview_video_saved_to); + } + } + }; DocumentSaveClickHandler::SaveAndTrack( _message ? _message->fullId() : FullMsgId(), _document, - DocumentSaveClickHandler::Mode::ToFile); - _documentLoadingTo = _document->loadingFilePath(); - if (_stories && _documentLoadingTo.isEmpty()) { - toName = _document->filepath(true); - if (!toName.isEmpty()) { - showSaveMsgToast( - toName, - tr::lng_mediaview_video_saved_to); - } - } + DocumentSaveClickHandler::Mode::ToFile, + crl::guard(_widget, checkSaveStarted)); } else { _saveVisible = computeSaveButtonVisible(); update(_saveNavOver); @@ -5172,7 +5179,9 @@ void OverlayWidget::handleWheelEvent(not_null e) { } void OverlayWidget::setZoomLevel(int newZoom, bool force) { - if (_stories || (!force && _zoom == newZoom)) { + if (_stories + || (!force && _zoom == newZoom) + || (_fullScreenVideo && newZoom != kZoomToScreenLevel)) { return; } diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index f13a96bce35255..f842fd808afecb 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -16,6 +16,7 @@ For license and copyright information please follow this link: #include "base/random.h" #include +#include #include #include @@ -212,6 +213,9 @@ LinuxIntegration::LinuxIntegration() const Glib::VariantBase &value) { if (group == "org.freedesktop.appearance" && key == "color-scheme") { +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + QWindowSystemInterface::handleThemeChange(); +#else // Qt >= 6.5.0 try { const auto ivalue = value.get_dynamic(); @@ -220,6 +224,7 @@ LinuxIntegration::LinuxIntegration() }); } catch (...) { } +#endif // Qt < 6.5.0 } }) { LOG(("Icon theme: %1").arg(QIcon::themeName())); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 4b95c2a5d92fa5..4f53c9a3326fc1 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -316,21 +316,15 @@ sessionLocationTop: 54px; sessionCurrentSkip: 8px; sessionSubtitleSkip: 14px; sessionInfoFg: windowSubTextFg; -sessionTerminateTop: 9px; -sessionTerminateSkip: 12px; +sessionTerminateTop: 8px; +sessionTerminateSkip: 11px; sessionTerminate: IconButton { - width: 20px; - height: 20px; + width: 34px; + height: 34px; icon: smallCloseIcon; iconOver: smallCloseIconOver; - iconPosition: point(5px, 5px); - - rippleAreaPosition: point(0px, 0px); - rippleAreaSize: 20px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + iconPosition: point(12px, 12px); } sessionIconWindows: icon{{ "settings/devices/device_desktop_win", historyPeerUserpicFg }}; sessionIconMac: icon{{ "settings/devices/device_desktop_mac", historyPeerUserpicFg }}; @@ -365,11 +359,12 @@ sessionDateLabel: FlatLabel(defaultFlatLabel) { align: align(top); } sessionDateSkip: 19px; -sessionValuePadding: margins(0px, 5px, 0px, 2px); +sessionValuePadding: margins(37px, 5px, 0px, 0px); sessionValueLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } sessionValueSkip: 8px; +sessionValueIconPosition: point(20px, 9px); sessionListItem: PeerListItem(defaultPeerListItem) { button: OutlineButton(defaultPeerListButton) { @@ -391,6 +386,21 @@ sessionList: PeerList(defaultPeerList) { item: sessionListItem; padding: margins(0px, 4px, 0px, 0px); } +websiteListItem: PeerListItem(sessionListItem) { + height: 72px; + photoPosition: point(18px, 10px); + namePosition: point(64px, 6px); + statusPosition: point(64px, 26px); + photoSize: 32px; +} +websiteList: PeerList(sessionList) { + item: websiteListItem; +} +websiteLocationTop: 46px; +websiteBigUserpic: UserpicButton(defaultUserpicButton) { + size: size(70px, 70px); + photoSize: 70px; +} settingsPhotoLeft: 22px; settingsPhotoTop: 8px; diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index e20d500db41d52..25a17d3f906569 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -13,6 +13,7 @@ For license and copyright information please follow this link: #include "api/api_self_destruct.h" #include "api/api_sensitive_content.h" #include "api/api_global_privacy.h" +#include "api/api_websites.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" #include "settings/cloud_password/settings_cloud_password_input.h" #include "settings/cloud_password/settings_cloud_password_start.h" @@ -22,6 +23,7 @@ For license and copyright information please follow this link: #include "settings/settings_local_passcode.h" #include "settings/settings_premium.h" // Settings::ShowPremium. #include "settings/settings_privacy_controllers.h" +#include "settings/settings_websites.h" #include "base/timer_rpl.h" #include "boxes/edit_privacy_box.h" #include "boxes/passcode_box.h" @@ -592,6 +594,44 @@ void SetupBlockedList( }, blockedPeers->lifetime()); } +void SetupWebsitesList( + not_null controller, + not_null container, + rpl::producer<> updateTrigger, + Fn showOther) { + std::move( + updateTrigger + ) | rpl::start_with_next([=] { + controller->session().api().websites().reload(); + }, container->lifetime()); + + auto count = controller->session().api().websites().totalValue(); + auto countText = rpl::duplicate( + count + ) | rpl::filter(rpl::mappers::_1 > 0) | rpl::map([](int count) { + return QString::number(count); + }); + + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto inner = wrap->entity(); + + AddButtonWithLabel( + inner, + tr::lng_settings_logged_in(), + std::move(countText), + st::settingsButton, + { &st::menuIconIpAddress } + )->addClickHandler([=] { + showOther(Websites::Id()); + }); + + wrap->toggleOn(std::move(count) | rpl::map(rpl::mappers::_1 > 0)); + wrap->finishAnimating(); +} + void SetupSessionsList( not_null controller, not_null container, @@ -603,7 +643,7 @@ void SetupSessionsList( controller->session().api().authorizations().reload(); }, container->lifetime()); - auto count = controller->session().api().authorizations().totalChanges( + auto count = controller->session().api().authorizations().totalValue( ) | rpl::map([](int count) { return count ? QString::number(count) : QString(); }); @@ -664,12 +704,17 @@ void SetupSecurity( container, rpl::duplicate(updateTrigger), showOther); + SetupLocalPasscode(controller, container, showOther); SetupBlockedList( controller, container, rpl::duplicate(updateTrigger), showOther); - SetupLocalPasscode(controller, container, showOther); + SetupWebsitesList( + controller, + container, + rpl::duplicate(updateTrigger), + showOther); SetupSessionsList( controller, container, diff --git a/Telegram/SourceFiles/settings/settings_websites.cpp b/Telegram/SourceFiles/settings/settings_websites.cpp new file mode 100644 index 00000000000000..495ffd6ff78918 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_websites.cpp @@ -0,0 +1,783 @@ +/* +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 "settings/settings_websites.h" + +#include "api/api_websites.h" +#include "apiwrap.h" +#include "boxes/peer_list_box.h" +#include "boxes/sessions_box.h" +#include "data/data_user.h" +#include "ui/boxes/confirm_box.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/controls/userpic_button.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" +#include "styles/style_menu_icons.h" + +namespace { + +constexpr auto kShortPollTimeout = 60 * crl::time(1000); +constexpr auto kMaxDeviceModelLength = 32; + +using EntryData = Api::Websites::Entry; + +class Row; + +class RowDelegate { +public: + virtual void rowUpdateRow(not_null row) = 0; +}; + +class Row final : public PeerListRow { +public: + Row(not_null delegate, const EntryData &data); + + void update(const EntryData &data); + void updateName(const QString &name); + + [[nodiscard]] EntryData data() const; + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + + QSize rightActionSize() const override { + return elementGeometry(2, 0).size(); + } + QMargins rightActionMargins() const override { + const auto rect = elementGeometry(2, 0); + return QMargins(0, rect.y(), -(rect.x() + rect.width()), 0); + } + + int elementsCount() const override; + QRect elementGeometry(int element, int outerWidth) const override; + bool elementDisabled(int element) const override; + bool elementOnlySelect(int element) const override; + void elementAddRipple( + int element, + QPoint point, + Fn updateCallback) override; + void elementsStopLastRipple() override; + void elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) override; + +private: + const not_null _delegate; + QImage _emptyUserpic; + Ui::PeerUserpicView _userpic; + Ui::Text::String _location; + EntryData _data; + +}; + +[[nodiscard]] QString JoinNonEmpty(QStringList list) { + list.erase(ranges::remove(list, QString()), list.end()); + return list.join(", "); +} + +[[nodiscard]] QString LocationAndDate(const EntryData &entry) { + return (entry.location.isEmpty() ? entry.ip : entry.location) + + (entry.hash + ? (QString::fromUtf8(" \xE2\x80\xA2 ") + entry.active) + : QString()); +} + +void InfoBox( + not_null box, + const EntryData &data, + Fn terminate) { + box->setWidth(st::boxWideWidth); + + const auto shown = box->lifetime().make_state>(); + box->setShowFinishedCallback([=] { + shown->fire({}); + }); + + const auto userpic = box->addRow( + object_ptr>( + box, + object_ptr( + box, + data.bot, + st::websiteBigUserpic)), + st::sessionBigCoverPadding)->entity(); + userpic->forceForumShape(true); + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto nameWrap = box->addRow( + object_ptr( + box, + st::sessionBigName.maxHeight)); + const auto name = Ui::CreateChild( + nameWrap, + rpl::single(data.bot->name()), + st::sessionBigName); + nameWrap->widthValue( + ) | rpl::start_with_next([=](int width) { + name->resizeToWidth(width); + name->move((width - name->width()) / 2, 0); + }, name->lifetime()); + + const auto domainWrap = box->addRow( + object_ptr( + box, + st::sessionDateLabel.style.font->height), + style::margins(0, 0, 0, st::sessionDateSkip)); + const auto domain = Ui::CreateChild( + domainWrap, + rpl::single(data.domain), + st::sessionDateLabel); + rpl::combine( + domainWrap->widthValue(), + domain->widthValue() + ) | rpl::start_with_next([=](int outer, int inner) { + domain->move((outer - inner) / 2, 0); + }, domain->lifetime()); + + using namespace Settings; + const auto container = box->verticalLayout(); + AddDivider(container); + AddSkip(container, st::sessionSubtitleSkip); + AddSubsectionTitle(container, tr::lng_sessions_info()); + + AddSessionInfoRow( + container, + tr::lng_sessions_browser(), + JoinNonEmpty({ data.browser, data.platform }), + st::menuIconDevices); + AddSessionInfoRow( + container, + tr::lng_sessions_ip(), + data.ip, + st::menuIconIpAddress); + AddSessionInfoRow( + container, + tr::lng_sessions_location(), + data.location, + st::menuIconAddress); + + AddSkip(container, st::sessionValueSkip); + if (!data.location.isEmpty()) { + AddDividerText(container, tr::lng_sessions_location_about()); + } + + box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); + if (const auto hash = data.hash) { + box->addLeftButton(tr::lng_settings_disconnect(), [=] { + const auto weak = Ui::MakeWeak(box.get()); + terminate(hash); + if (weak) { + box->closeBox(); + } + }, st::attentionBoxButton); + } +} + +Row::Row(not_null delegate, const EntryData &data) +: PeerListRow(data.hash) +, _delegate(delegate) +, _location(st::defaultTextStyle, LocationAndDate(data)) +, _data(data) { + setCustomStatus(_data.ip); +} + +void Row::update(const EntryData &data) { + _data = data; + setCustomStatus( + JoinNonEmpty({ _data.domain, _data.browser, _data.platform })); + refreshName(st::websiteListItem); + _location.setText(st::defaultTextStyle, LocationAndDate(_data)); + _delegate->rowUpdateRow(this); +} + +EntryData Row::data() const { + return _data; +} + +QString Row::generateName() { + return _data.bot->name(); +} + +QString Row::generateShortName() { + return _data.bot->shortName(); +} + +PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) { + const auto peer = _data.bot; + auto userpic = _userpic = peer->createUserpicView(); + return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { + const auto ratio = style::DevicePixelRatio(); + if (const auto cloud = peer->userpicCloudImage(userpic)) { + Ui::ValidateUserpicCache( + userpic, + cloud, + nullptr, + size * ratio, + true); + p.drawImage(QRect(x, y, size, size), userpic.cached); + } else { + if (_emptyUserpic.isNull()) { + _emptyUserpic = peer->generateUserpicImage( + _userpic, + size * ratio, + size * ratio * Ui::ForumUserpicRadiusMultiplier()); + } + p.drawImage(QRect(x, y, size, size), _emptyUserpic); + } + }; +} + +int Row::elementsCount() const { + return 2; +} + +QRect Row::elementGeometry(int element, int outerWidth) const { + switch (element) { + case 1: { + return QRect( + st::websiteListItem.namePosition.x(), + st::websiteLocationTop, + outerWidth, + st::normalFont->height); + } break; + case 2: { + const auto size = QSize( + st::sessionTerminate.width, + st::sessionTerminate.height); + const auto right = st::sessionTerminateSkip; + const auto top = st::sessionTerminateTop; + const auto left = outerWidth - right - size.width(); + return QRect(QPoint(left, top), size); + } break; + } + return QRect(); +} + +bool Row::elementDisabled(int element) const { + return !id() || (element == 1); +} + +bool Row::elementOnlySelect(int element) const { + return false; +} + +void Row::elementAddRipple( + int element, + QPoint point, + Fn updateCallback) { +} + +void Row::elementsStopLastRipple() { +} + +void Row::elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) { + const auto geometry = elementGeometry(2, outerWidth); + const auto position = geometry.topLeft() + + st::sessionTerminate.iconPosition; + const auto &icon = (selectedElement == 2) + ? st::sessionTerminate.iconOver + : st::sessionTerminate.icon; + icon.paint(p, position.x(), position.y(), outerWidth); + + p.setFont(st::normalFont); + p.setPen(st::sessionInfoFg); + const auto locationLeft = st::websiteListItem.namePosition.x(); + const auto available = outerWidth - locationLeft; + _location.drawLeftElided( + p, + locationLeft, + st::websiteLocationTop, + available, + outerWidth); +} + +class Content : public Ui::RpWidget { +public: + Content( + QWidget*, + not_null controller); + + void setupContent(); + +protected: + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + +private: + class Inner; + class ListController; + + void shortPoll(); + void parse(const Api::Websites::List &list); + + void terminate( + Fn sendRequest, + rpl::producer title, + rpl::producer text, + QString blockText = QString()); + void terminateOne(uint64 hash); + void terminateAll(); + + const not_null _controller; + const not_null _websites; + + rpl::variable _loading = false; + Api::Websites::List _data; + + object_ptr _inner; + QPointer _terminateBox; + + base::Timer _shortPollTimer; + +}; + +class Content::ListController final + : public PeerListController + , public RowDelegate + , public base::has_weak_ptr { +public: + explicit ListController(not_null session); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void rowElementClicked(not_null row, int element) override; + + void rowUpdateRow(not_null row) override; + + void showData(gsl::span items); + rpl::producer itemsCount() const; + rpl::producer terminateRequests() const; + [[nodiscard]] rpl::producer showRequests() const; + + [[nodiscard]] static std::unique_ptr Add( + not_null container, + not_null session, + style::margins margins = {}); + +private: + const not_null _session; + + rpl::event_stream _terminateRequests; + rpl::event_stream _itemsCount; + rpl::event_stream _showRequests; + +}; + +class Content::Inner : public Ui::RpWidget { +public: + Inner( + QWidget *parent, + not_null controller); + + void showData(const Api::Websites::List &data); + [[nodiscard]] rpl::producer showRequests() const; + [[nodiscard]] rpl::producer terminateOne() const; + [[nodiscard]] rpl::producer<> terminateAll() const; + +private: + void setupContent(); + + const not_null _controller; + QPointer _terminateAll; + std::unique_ptr _list; + +}; + +Content::Content( + QWidget*, + not_null controller) +: _controller(controller) +, _websites(&controller->session().api().websites()) +, _inner(this, controller) +, _shortPollTimer([=] { shortPoll(); }) { +} + +void Content::setupContent() { + _inner->heightValue( + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](int height) { + resize(width(), height); + }, _inner->lifetime()); + + _inner->showRequests( + ) | rpl::start_with_next([=](const EntryData &data) { + _controller->show(Box( + InfoBox, + data, + [=](uint64 hash) { terminateOne(hash); })); + }, lifetime()); + + _inner->terminateOne( + ) | rpl::start_with_next([=](uint64 hash) { + terminateOne(hash); + }, lifetime()); + + _inner->terminateAll( + ) | rpl::start_with_next([=] { + terminateAll(); + }, lifetime()); + + _loading.changes( + ) | rpl::start_with_next([=](bool value) { + _inner->setVisible(!value); + }, lifetime()); + + _websites->listValue( + ) | rpl::start_with_next([=](const Api::Websites::List &list) { + parse(list); + }, lifetime()); + + _loading = true; + shortPoll(); +} + +void Content::parse(const Api::Websites::List &list) { + _loading = false; + + _data = list; + + ranges::sort(_data, std::greater<>(), &EntryData::activeTime); + + _inner->showData(_data); + + _shortPollTimer.callOnce(kShortPollTimeout); +} + +void Content::resizeEvent(QResizeEvent *e) { + RpWidget::resizeEvent(e); + + _inner->resize(width(), _inner->height()); +} + +void Content::paintEvent(QPaintEvent *e) { + RpWidget::paintEvent(e); + + Painter p(this); + + if (_loading.current()) { + p.setFont(st::noContactsFont); + p.setPen(st::noContactsColor); + p.drawText( + QRect(0, 0, width(), st::noContactsHeight), + tr::lng_contacts_loading(tr::now), + style::al_center); + } +} + +void Content::shortPoll() { + const auto left = kShortPollTimeout + - (crl::now() - _websites->lastReceivedTime()); + if (left > 0) { + parse(_websites->list()); + _shortPollTimer.cancel(); + _shortPollTimer.callOnce(left); + } else { + _websites->reload(); + } + update(); +} + +void Content::terminate( + Fn sendRequest, + rpl::producer title, + rpl::producer text, + QString blockText) { + if (const auto strong = _terminateBox.data()) { + strong->deleteLater(); + } + auto box = Box([=](not_null box) { + auto &lifetime = box->lifetime(); + const auto block = lifetime.make_state(nullptr); + const auto callback = crl::guard(this, [=] { + const auto blocked = (*block) && (*block)->checked(); + if (_terminateBox) { + _terminateBox->closeBox(); + _terminateBox = nullptr; + } + sendRequest(blocked); + }); + Ui::ConfirmBox(box, { + .text = rpl::duplicate(text), + .confirmed = callback, + .confirmText = tr::lng_settings_disconnect(), + .confirmStyle = &st::attentionBoxButton, + .title = rpl::duplicate(title), + }); + if (!blockText.isEmpty()) { + *block = box->addRow(object_ptr(box, blockText)); + } + }); + _terminateBox = Ui::MakeWeak(box.data()); + _controller->show(std::move(box)); +} + +void Content::terminateOne(uint64 hash) { + const auto weak = Ui::MakeWeak(this); + const auto i = ranges::find(_data, hash, &EntryData::hash); + if (i == end(_data)) { + return; + } + + const auto bot = i->bot; + auto callback = [=](bool block) { + auto done = crl::guard(weak, [=](const MTPBool &result) { + _data.erase( + ranges::remove(_data, hash, &EntryData::hash), + end(_data)); + _inner->showData(_data); + }); + auto fail = crl::guard(weak, [=](const MTP::Error &error) { + }); + _websites->requestTerminate( + std::move(done), + std::move(fail), + hash, + block ? bot.get() : nullptr); + }; + terminate( + std::move(callback), + tr::lng_settings_disconnect_title(), + tr::lng_settings_disconnect_sure(lt_domain, rpl::single(i->domain)), + tr::lng_settings_disconnect_block(tr::now, lt_name, bot->name())); +} + +void Content::terminateAll() { + const auto weak = Ui::MakeWeak(this); + auto callback = [=](bool block) { + const auto reset = crl::guard(weak, [=] { + _websites->cancelCurrentRequest(); + _websites->reload(); + }); + _websites->requestTerminate( + [=](const MTPBool &result) { reset(); }, + [=](const MTP::Error &result) { reset(); }); + _loading = true; + }; + terminate( + std::move(callback), + tr::lng_settings_disconnect_all_title(), + tr::lng_settings_disconnect_all_sure()); +} + +Content::Inner::Inner( + QWidget *parent, + not_null controller) +: RpWidget(parent) +, _controller(controller) { + resize(width(), st::noContactsHeight); + setupContent(); +} + +void Content::Inner::setupContent() { + using namespace Settings; + using namespace rpl::mappers; + + const auto content = Ui::CreateChild(this); + + const auto session = &_controller->session(); + const auto terminateWrap = content->add( + object_ptr>( + content, + object_ptr(content)))->setDuration(0); + const auto terminateInner = terminateWrap->entity(); + _terminateAll = terminateInner->add( + CreateButton( + terminateInner, + tr::lng_settings_disconnect_all(), + st::infoBlockButton, + { .icon = &st::infoIconBlock })); + AddSkip(terminateInner); + AddDividerText( + terminateInner, + tr::lng_settings_logged_in_description()); + + const auto listWrap = content->add( + object_ptr>( + content, + object_ptr(content)))->setDuration(0); + const auto listInner = listWrap->entity(); + AddSkip(listInner, st::sessionSubtitleSkip); + AddSubsectionTitle(listInner, tr::lng_settings_logged_in_title()); + _list = ListController::Add(listInner, session); + AddSkip(listInner); + + const auto skip = st::noContactsHeight / 2; + const auto placeholder = content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_settings_logged_in_description(), + st::boxDividerLabel), + st::settingsDividerLabelPadding + QMargins(0, skip, 0, skip)) + )->setDuration(0); + + terminateWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0)); + listWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0)); + placeholder->toggleOn(_list->itemsCount() | rpl::map(_1 == 0)); + + Ui::ResizeFitChild(this, content); +} + +void Content::Inner::showData(const Api::Websites::List &data) { + _list->showData(data); +} + +rpl::producer<> Content::Inner::terminateAll() const { + return _terminateAll->clicks() | rpl::to_empty; +} + +rpl::producer Content::Inner::terminateOne() const { + return _list->terminateRequests(); +} + +rpl::producer Content::Inner::showRequests() const { + return _list->showRequests(); +} + +Content::ListController::ListController( + not_null session) +: _session(session) { +} + +Main::Session &Content::ListController::session() const { + return *_session; +} + +void Content::ListController::prepare() { +} + +void Content::ListController::rowClicked( + not_null row) { + _showRequests.fire_copy(static_cast(row.get())->data()); +} + +void Content::ListController::rowElementClicked( + not_null row, + int element) { + if (element == 2) { + if (const auto hash = static_cast(row.get())->data().hash) { + _terminateRequests.fire_copy(hash); + } + } +} + +void Content::ListController::rowUpdateRow(not_null row) { + delegate()->peerListUpdateRow(row); +} + +void Content::ListController::showData( + gsl::span items) { + auto index = 0; + auto positions = base::flat_map(); + positions.reserve(items.size()); + for (const auto &entry : items) { + const auto id = entry.hash; + positions.emplace(id, index++); + if (const auto row = delegate()->peerListFindRow(id)) { + static_cast(row)->update(entry); + } else { + delegate()->peerListAppendRow( + std::make_unique(this, entry)); + } + } + for (auto i = 0; i != delegate()->peerListFullRowsCount();) { + const auto row = delegate()->peerListRowAt(i); + if (positions.contains(row->id())) { + ++i; + continue; + } + delegate()->peerListRemoveRow(row); + } + delegate()->peerListSortRows([&]( + const PeerListRow &a, + const PeerListRow &b) { + return positions[a.id()] < positions[b.id()]; + }); + delegate()->peerListRefreshRows(); + _itemsCount.fire(delegate()->peerListFullRowsCount()); +} + +rpl::producer Content::ListController::itemsCount() const { + return _itemsCount.events_starting_with( + delegate()->peerListFullRowsCount()); +} + +rpl::producer Content::ListController::terminateRequests() const { + return _terminateRequests.events(); +} + +rpl::producer Content::ListController::showRequests() const { + return _showRequests.events(); +} + +auto Content::ListController::Add( + not_null container, + not_null session, + style::margins margins) +-> std::unique_ptr { + auto &lifetime = container->lifetime(); + const auto delegate = lifetime.make_state< + PeerListContentDelegateSimple + >(); + auto controller = std::make_unique(session); + controller->setStyleOverrides(&st::websiteList); + const auto content = container->add( + object_ptr( + container, + controller.get()), + margins); + delegate->setContent(content); + controller->setDelegate(delegate); + return controller; +} + +} // namespace + +namespace Settings { + +Websites::Websites( + QWidget *parent, + not_null controller) +: Section(parent) { + setupContent(controller); +} + +rpl::producer Websites::title() { + return tr::lng_settings_connected_title(); +} + +void Websites::setupContent(not_null controller) { + const auto container = Ui::CreateChild(this); + AddSkip(container); + const auto content = container->add( + object_ptr(container, controller)); + content->setupContent(); + + Ui::ResizeFitChild(this, container); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_websites.h b/Telegram/SourceFiles/settings/settings_websites.h new file mode 100644 index 00000000000000..4b44bd8a9ebfde --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_websites.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 "settings/settings_common.h" + +namespace Settings { + +class Websites : public Section { +public: + Websites( + QWidget *parent, + not_null controller); + + [[nodiscard]] rpl::producer title() override; + +private: + void setupContent(not_null controller); + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index bb0888d1380a4b..cb8fe49c5cae5b 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -9,6 +9,8 @@ For license and copyright information please follow this link: #include "dialogs/dialogs_key.h" #include "data/data_drafts.h" +#include "data/data_forum.h" +#include "data/data_forum_topic.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_changes.h" @@ -555,6 +557,7 @@ QString InterpretSendPath( f.close(); const auto lines = content.split('\n'); auto toId = PeerId(0); + auto topicRootId = MsgId(0); auto filePath = QString(); auto caption = QString(); for (const auto &line : lines) { @@ -570,6 +573,11 @@ QString InterpretSendPath( line, u"channel: "_q.size()).toULongLong(); toId = peerFromChannel(channelId); + } else if (line.startsWith(u"topic: "_q)) { + const auto topicId = base::StringViewMid( + line, + u"topic: "_q.size()).toULongLong(); + topicRootId = MsgId(topicId); } else if (line.startsWith(u"file: "_q)) { const auto path = line.mid(u"file: "_q.size()); if (!QFile(path).exists()) { @@ -585,21 +593,33 @@ QString InterpretSendPath( } } const auto history = window->session().data().historyLoaded(toId); + const auto sendTo = [=](not_null thread) { + window->showThread(thread); + const auto premium = thread->session().user()->isPremium(); + thread->session().api().sendFiles( + Storage::PrepareMediaList( + QStringList(filePath), + st::sendMediaPreviewSize, + premium), + SendMediaType::File, + { caption }, + nullptr, + Api::SendAction(thread)); + }; if (!history) { return "App Error: Could not find channel with id: " + QString::number(peerToChannel(toId).bare); + } else if (const auto forum = history->asForum()) { + forum->requestTopic(topicRootId, [=] { + if (const auto forum = history->asForum()) { + if (const auto topic = forum->topicFor(topicRootId)) { + sendTo(topic); + } + } + }); + } else if (!topicRootId) { + sendTo(history); } - window->showPeerHistory(history); - const auto premium = window->session().user()->isPremium(); - history->session().api().sendFiles( - Storage::PrepareMediaList( - QStringList(filePath), - st::sendMediaPreviewSize, - premium), - SendMediaType::File, - { caption }, - nullptr, - Api::SendAction(history)); return QString(); } diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp index c048b2354f89e6..070f84b294b1d7 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp @@ -17,18 +17,26 @@ void ConfirmBox(not_null box, ConfirmBoxArgs &&args) { const auto weak = Ui::MakeWeak(box); const auto lifetime = box->lifetime().make_state(); - v::match(args.text, [](v::null_t) { - }, [&](auto &&) { + const auto withTitle = !v::is_null(args.title); + if (withTitle) { + box->setTitle(v::text::take_marked(std::move(args.title))); + } + + if (!v::is_null(args.text)) { + const auto padding = st::boxPadding; + const auto use = withTitle + ? QMargins(padding.left(), 0, padding.right(), padding.bottom()) + : padding; const auto label = box->addRow( object_ptr( box.get(), v::text::take_marked(std::move(args.text)), args.labelStyle ? *args.labelStyle : st::boxLabel), - st::boxPadding); + use); if (args.labelFilter) { label->setClickHandlerFilter(std::move(args.labelFilter)); } - }); + } const auto prepareCallback = [&](ConfirmBoxArgs::Callback &callback) { return [=, confirmed = std::move(callback)]() { diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.h b/Telegram/SourceFiles/ui/boxes/confirm_box.h index 297909f69355ad..b66be3d3a7cb2b 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.h +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.h @@ -31,6 +31,8 @@ struct ConfirmBoxArgs { const style::FlatLabel *labelStyle = nullptr; Fn labelFilter; + v::text::data title = v::null; + bool inform = false; // If strict cancel is set the cancel.callback() is only called // if the cancel button was pressed. diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index df5f98e04d6520..439c94523e915b 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -871,6 +871,11 @@ void UserpicButton::switchChangePhotoOverlay( } } +void UserpicButton::forceForumShape(bool force) { + _forceForumShape = force; + prepare(); +} + void UserpicButton::showSavedMessagesOnSelf(bool enabled) { if (_showSavedMessagesOnSelf != enabled) { _showSavedMessagesOnSelf = enabled; @@ -1003,7 +1008,26 @@ void UserpicButton::prepareUserpicPixmap() { _userpic = CreateSquarePixmap(size, [&](Painter &p) { if (_userpicHasImage) { if (_showPeerUserpic) { - _peer->paintUserpic(p, _userpicView, 0, 0, size); + if (useForumShape()) { + const auto ratio = style::DevicePixelRatio(); + if (const auto cloud = _peer->userpicCloudImage(_userpicView)) { + Ui::ValidateUserpicCache( + _userpicView, + cloud, + nullptr, + size * ratio, + true); + p.drawImage(QRect(0, 0, size, size), _userpicView.cached); + } else { + const auto empty = _peer->generateUserpicImage( + _userpicView, + size * ratio, + size * ratio * Ui::ForumUserpicRadiusMultiplier()); + p.drawImage(QRect(0, 0, size, size), empty); + } + } else { + _peer->paintUserpic(p, _userpicView, 0, 0, size); + } } else if (_nonPersonalView) { using Size = Data::PhotoSize; if (const auto full = _nonPersonalView->image(Size::Large)) { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index ef0e8f80028aa7..8d2eb2adf9598d 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -94,6 +94,7 @@ class UserpicButton final : public RippleButton { bool enabled, Fn chosen); void showSavedMessagesOnSelf(bool enabled); + void forceForumShape(bool force); // Role::ChoosePhoto or Role::ChangePhoto [[nodiscard]] rpl::producer chosenImages() const { diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 7aaf3221334429..5f19251c026f0b 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -135,6 +135,8 @@ menuIconAntispam: icon {{ "menu/antispam", menuIconColor }}; menuIconChatDiscuss: icon {{ "menu/chat_discuss", menuIconColor }}; menuIconBotCommands: icon {{ "menu/bot_commands", menuIconColor }}; menuIconPremium: icon {{ "menu/premium", menuIconColor }}; +menuIconIpAddress: icon {{ "menu/ip_address", menuIconColor }}; +menuIconAddress: icon {{ "menu/payment_address", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index e83f2df0e8ba5f..49316a1998bd7c 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -59,7 +59,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin edafe1a2484cd6ac9be357aa1e4cda61bf6a8b9a \ + && git fetch --depth=1 origin dbd8a5b00f5a0e7e09b11878b9e46afeb4b593c0 \ && git reset --hard FETCH_HEAD \ && rm -rf .git diff --git a/Telegram/build/updates.py b/Telegram/build/updates.py index 85dbda59a67893..2b5418605e94b8 100644 --- a/Telegram/build/updates.py +++ b/Telegram/build/updates.py @@ -11,6 +11,7 @@ nextUuid = False building = True composing = False +conf = 'Release' for arg in sys.argv: if nextLast: lastCommit = arg @@ -32,6 +33,8 @@ nextDate = True elif arg == 'request_uuid': nextUuid = True + elif arg == 'debug': + conf = 'Debug' def finish(code, error = ''): if error != '': @@ -53,9 +56,9 @@ def finish(code, error = ''): archive = 'tdesktop_macOS_' + today + '.zip' if building: - print('Building Release version for OS X 10.12+..') + print('Building ' + conf + ' version for OS X 10.12+..') - if os.path.exists('../out/Release/' + outputFolder): + if os.path.exists('../out/' + conf + '/' + outputFolder): finish(1, 'Todays updates version exists.') if uuid == '': @@ -65,11 +68,11 @@ def finish(code, error = ''): os.chdir('../out') if uuid == '': - result = subprocess.call('cmake --build . --config Release --target Telegram', shell=True) + result = subprocess.call('cmake --build . --config ' + conf + ' --target Telegram', shell=True) if result != 0: finish(1, 'While building Telegram.') - os.chdir('Release') + os.chdir(conf); if uuid == '': if not os.path.exists('Telegram.app'): finish(1, 'Telegram.app not found.') @@ -193,7 +196,7 @@ def finish(code, error = ''): print('NB! Notarization log not found.') finish(0) -commandPath = scriptPath + '/../../out/Release/' + outputFolder + '/command.txt' +commandPath = scriptPath + '/../../out/' + conf + '/' + outputFolder + '/command.txt' if composing: templatePath = scriptPath + '/../../../DesktopPrivate/updates_template.txt' @@ -235,7 +238,7 @@ def finish(code, error = ''): for line in template: if line.startswith('//'): continue - line = line.replace('{path}', scriptPath + '/../../out/Release/' + outputFolder + '/' + archive) + line = line.replace('{path}', scriptPath + '/../../out/' + conf + '/' + outputFolder + '/' + archive) line = line.replace('{caption}', 'TDesktop at ' + today.replace('_', '.') + ':\n\n' + changelog) f.write(line) print('\n\nEdit:\n') @@ -262,9 +265,9 @@ def finish(code, error = ''): print('vi ' + commandPath) finish(1, 'Too large.') -if not os.path.exists('../out/Release/' + outputFolder + '/' + archive): +if not os.path.exists('../out/' + conf + '/' + outputFolder + '/' + archive): finish(1, 'Not built yet.') -subprocess.call(scriptPath + '/../../out/Release/Telegram.app/Contents/MacOS/Telegram -sendpath interpret://' + scriptPath + '/../../out/Release/' + outputFolder + '/command.txt', shell=True) +subprocess.call(scriptPath + '/../../out/' + conf + '/Telegram.app/Contents/MacOS/Telegram -sendpath interpret://' + scriptPath + '/../../out/' + conf + '/' + outputFolder + '/command.txt', shell=True) finish(0) diff --git a/Telegram/lib_base b/Telegram/lib_base index 9c68fbe3cc4242..0919789bf555c6 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 9c68fbe3cc4242a5f5f76dfda8ceab511cc08c70 +Subproject commit 0919789bf555c651e32bdbcabc397dbacb6a2545 diff --git a/Telegram/lib_ui b/Telegram/lib_ui index b3580f7987d81b..a6d7e3f5457b29 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit b3580f7987d81b0c7aecee99f7f70f7873f6b35c +Subproject commit a6d7e3f5457b29b563941b24cd844ca26ac0d077 diff --git a/cmake b/cmake index ae0986c9efd28c..bd556e2a16d34a 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit ae0986c9efd28c61783cfe106f8bd1b0ba3b3920 +Subproject commit bd556e2a16d34ad876054fb3ab25e83481af652a