diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 10b47447d..f15f85198 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/menu/ip_address.png b/Telegram/Resources/icons/menu/ip_address.png new file mode 100644 index 000000000..3aa87b0aa 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 000000000..1184e10b0 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 000000000..3cee2b09d 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 000000000..a7cc0eb69 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 000000000..12daa492c 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 000000000..3172912c6 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 4253a9792..2ac849314 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."; diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index 6d82a1d11..78e463c11 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 96819edf1..5e2a41c9f 100644 --- a/Telegram/SourceFiles/api/api_authorizations.h +++ b/Telegram/SourceFiles/api/api_authorizations.h @@ -38,9 +38,9 @@ public: [[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 @@ public: [[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 000000000..855056675 --- /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 000000000..1551ae4d4 --- /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 5ee484778..e71e03953 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 5815fd3c0..954500f76 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 @@ public: [[nodiscard]] Api::Transcribes &transcribes(); [[nodiscard]] Api::Premium &premium(); [[nodiscard]] Api::Usernames &usernames(); + [[nodiscard]] Api::Websites &websites(); void updatePrivacyLastSeens(); @@ -693,6 +695,7 @@ private: 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/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index 55b5f499c..5a45ca0de 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.cpp +++ b/Telegram/SourceFiles/boxes/sessions_box.cpp @@ -35,10 +35,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 @@ public: 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 d735189a5..47d03de27 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.h +++ b/Telegram/SourceFiles/boxes/sessions_box.h @@ -7,12 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #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 @@ private: }; +void AddSessionInfoRow( + not_null container, + rpl::producer label, + const QString &value, + const style::icon &icon); + } // namespace Settings - -class SessionsBox : public Ui::BoxContent { -public: - SessionsBox(QWidget*, not_null controller); - -protected: - void prepare() override; - -private: - const not_null _controller; - -}; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 4b95c2a5d..4f53c9a33 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 e20d500db..25a17d3f9 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 000000000..495ffd6ff --- /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 000000000..4b44bd8a9 --- /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/ui/boxes/confirm_box.cpp b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp index c048b2354..070f84b29 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 297909f69..b66be3d3a 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 df5f98e04..439c94523 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 ef0e8f800..8d2eb2adf 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -94,6 +94,7 @@ public: 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 7aaf32213..5f19251c0 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);