From 92fec8304e4bb14c36842e1c11d2c8ff22dc6608 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 18 Aug 2023 17:03:50 +0200 Subject: [PATCH] Implement connected websites section. --- Telegram/CMakeLists.txt | 4 + Telegram/Resources/icons/menu/ip_address.png | Bin 0 -> 701 bytes .../Resources/icons/menu/ip_address@2x.png | Bin 0 -> 1274 bytes .../Resources/icons/menu/ip_address@3x.png | Bin 0 -> 2002 bytes .../Resources/icons/menu/payment_address.png | Bin 0 -> 584 bytes .../icons/menu/payment_address@2x.png | Bin 0 -> 1195 bytes .../icons/menu/payment_address@3x.png | Bin 0 -> 1834 bytes Telegram/Resources/langs/lang.strings | 15 +- .../SourceFiles/api/api_authorizations.cpp | 65 +- Telegram/SourceFiles/api/api_authorizations.h | 6 +- Telegram/SourceFiles/api/api_websites.cpp | 138 +++ Telegram/SourceFiles/api/api_websites.h | 62 ++ Telegram/SourceFiles/apiwrap.cpp | 8 +- Telegram/SourceFiles/apiwrap.h | 3 + Telegram/SourceFiles/boxes/sessions_box.cpp | 121 +-- Telegram/SourceFiles/boxes/sessions_box.h | 25 +- Telegram/SourceFiles/settings/settings.style | 34 +- .../settings/settings_privacy_security.cpp | 49 +- .../settings/settings_websites.cpp | 783 ++++++++++++++++++ .../SourceFiles/settings/settings_websites.h | 27 + Telegram/SourceFiles/ui/boxes/confirm_box.cpp | 16 +- Telegram/SourceFiles/ui/boxes/confirm_box.h | 2 + .../ui/controls/userpic_button.cpp | 26 +- .../SourceFiles/ui/controls/userpic_button.h | 1 + Telegram/SourceFiles/ui/menu_icons.style | 2 + 25 files changed, 1261 insertions(+), 126 deletions(-) create mode 100644 Telegram/Resources/icons/menu/ip_address.png create mode 100644 Telegram/Resources/icons/menu/ip_address@2x.png create mode 100644 Telegram/Resources/icons/menu/ip_address@3x.png create mode 100644 Telegram/Resources/icons/menu/payment_address.png create mode 100644 Telegram/Resources/icons/menu/payment_address@2x.png create mode 100644 Telegram/Resources/icons/menu/payment_address@3x.png create mode 100644 Telegram/SourceFiles/api/api_websites.cpp create mode 100644 Telegram/SourceFiles/api/api_websites.h create mode 100644 Telegram/SourceFiles/settings/settings_websites.cpp create mode 100644 Telegram/SourceFiles/settings/settings_websites.h 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 0000000000000000000000000000000000000000..3aa87b0aa5c29c1ab9d6de18ab7751fa5ed2db6d GIT binary patch literal 701 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfl1xd#WBP} z@aYum?4&@ER%2r+ZPz2m8~lSJxs^PmE}pvNB^X`bnDLrlz)64aLl!+>bJG$ZOh*mmw!LMPJe#w%&^tG?S~s34!r*Q z=;ha{vuT^-*1!M$`+U&4(y3lkPo`|U{WfW%ME~)WO$-4pP7>>i^LECq&o_~hmzUod zp)=7V$87fd=d2A%LWdgOnuP~eh%U?9o^3We@czAfmtJn+U|x{mB(dRdR(#HE@h&a3 z$uCQ+Y;9~hoES6~t!U-l%+a~L=!VtYJiGbfT2uX&JD+*~-Cb#7h?eTzoiXdeR`1=n zZ_$sMeHAu&Tca%Zy>_~ou_Z*S*G*Y)Lxhf0Z<^KIQ%-#Cj|(i;glR9dn0qe8C~|Gs z(ue9AV%`4!{y|GK76wcZVQ`(|wKGP|_0oEwn>pJ~rYLEuADur*ghBMUx{FkkNM-<+ zr)R_O-@hM!tk854)Sv3TMr796G`;EG$F9BJdNE@P$N;XiAeGRnEmyM&S>*cNk7PJ+ zjgsYITY5Eb`{tDg6BI-WZy2o-N$LxTkBd9oAhE)xy>@%N3# z5|3skJ?Tqt%Pu#vH9NL22DoUr*WI4rC3@j%2T!~6EmdKI;Vst0M*+SvH$=8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1184e10b059654e377a4a6567bfcc9a982016163 GIT binary patch literal 1274 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/menu/payment_address.png b/Telegram/Resources/icons/menu/payment_address.png new file mode 100644 index 0000000000000000000000000000000000000000..a7cc0eb69f039d011f81cb271ee5ff4ab2385788 GIT binary patch literal 584 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgfY?G&pV~B;| z)hV|9jE*wx$9G=nnC`MVxHD?ugx_3x8e5%m6h-zi_V!&;pYTJ(WkrSvS72=6(qM&y zw%diXbvDP8oA=&7{rUdZ&67nR-CyIOaw*3wee=!fI|U*R-M4kv8g(~s`{9QhB6Rl6 z8F>TOW<6|aOKawb16pchZU!K-Cf@bbn#_Id&}=JdeeK| z4s!}Mon_km_x|dMDMlyte*CSQS!E-aW_PCbz$qC9o*!8erf*(U*lZJO3txS8H_*z} zvwYMJOmp3{>e%zoxn|PZC!c?QTD0^2`_oJA=3ReXy3$hUfLLcmAg{k%uiI6Z7<6`h(kNM=a^w3347N+Sh)^Mz3IM?`i{q48&{Fcx4dp=ER3Db_X@6 zvwv}~oAMgofVWqdd+RPqvzUDrXlC5{?f2j3H`>psVP4w*c*4zjqSrvN=;`X`vd$@? F2>^w2?e72p literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..12daa492c6589410c131b693504ffe0f50c43044 GIT binary patch literal 1195 zcmV;c1XTNpP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NG6-h)vR9Fe^m`x~jQ5eT(e3j&* z*!YN)g_1Q%QHVlzN?EY55?edHl)_GWOARR-wo+0k8%1`CY$Vj1LdokR7QDaasWaoA zJ9o~xb60P37Sp-U^Z)JP~H9y1Kfpt*xM-Km^y<*JER2TU%SUWhK}1jHaijzrMa?R`&MxIy*aECx9R+ z1fs-(5qt76%oX4^Ha27c=A)yd!^DdWvAD1YS=0Rdyl}bi;^JbHYD9xrO%)JxqnC1U za8Rn*-QCT~%JP!B<*;BB0w_0$O)Nh@|L*P%+}YV#VPT<-HKxUQetr%F6u?-*M{jm^ zR&K7TsmX_3S+ENSpbA=dST9dcPk`3f*R9Q(0SG97u4_}1y9K(tyXCgnj@|Mh7S71X z2wU^tXrserqSXXMXkcAw$%88^DW5+GE%Ph015$E9cUC`J&SI5^N%rS3#GTm)Pp->vc05H^Hv1F8z@a zBqaieQBZ<)q>L`7rlxoVY-Jf4&bobkse#Bg3)X0OUBtao5KZ40nHbw;uQo=RLVdX8 z5p{GdEG+11+l>(j_mOHSp1-IEGTmfM)^Gi%G{Mu+27wUH#aaakWl2Az$gSzo-Ey}vIqD5{k^uf z)=T#@hZqaS;NKN_C4G5$0nH*eoOxt&@?*sQv(zB2Mn^|w@+T%HOnAsdkI_VLtzq>Z z92_vl=jW$>m^g3^396BLtZf=h8XmoUu`Xj(KtLp=Z@Vc)4LKq$%jwf;{wIcBGM?gHv zu3uDCba8RP*nD%(ehAIY&G+~B z@{Ht@J&<|w!wSk%QBkq9w8S&w?2JSr<>lo;k<*;?4E!%M@DC}iO=?%NbsYcz002ov JPDHLkV1k{B7kK~x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3172912c685a79d6efc975c7b8a6ed8a6dfdd656 GIT binary patch literal 1834 zcmV+_2i5qAP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS>lu1NERA>e5noCGlTNKAN#hxaT zL0u7p2*eN}d=Qirkq%4;PI{1*h$7X{iAfF(dO!msibNeYP?U>egsNl1u);xiDRf%pu>XJBOp z5?4&4R$^jeQBhHOdHMeR`~Uj&LE)cYpLg%x4GauCd-iN;Y00Q^WMrkKr4JrF`0(Ka z8GC+!0VX7}bW9K*J9g~p)2E)rI06VH+(Z`tahSOj^YZd;-n>~>R%Xv^W@ct$VnSVj zjH;_LGc$ABwrzIsFJOC47A%PZh|0E`nTefVmBs4lYdh+DSwP_GY zxLzLQNUV)*?NMAyj~+eJ^XTmC+`M`7+8GlP2!}-HM8fe(4x{1`d;R)#$>7J2A6Kqi z@d)re3>Abzj*#!r(7=lqFN*NbpFhu>ITM<46#<0MiUe}2h9mm2YuB!?U%!fKOG`@> zLePo?a)?HuvWoNW-Mb>$-QB%v%;*=$MF>%&+%~d<2M;bREGWHCpFX9frP(0H9FQZS zv=Bww7`2_Kx3^bjv9Yny4lwIMLgbJ(OTNOkbLY;9ED3nx0|V%8W@l%W!0_;}XZ+ElN2jKy zEX7#90s!Vog`5f`0rO2(ety2B<$3Sor?6DThVt*_k7WZ2%=35C?`fprIT67Cr%#`j z!m6vQ-GlWjkewI@1UjCtIE4+cK5Dm-6EJDm0iis!aKe84_;D9576w7G#mvaau!DE+ z-mTZ>zLp>-g#MdSzpG+uYO3V-{{4Hg?S?+74Gj&GlaqGL=g*((>+8j@hlo=QT~1F! z0FH*=!npT4#@n4GB_+!F@#9C%q3i(VVF-Y-B_tMD{51YzyBmHB(Jk~wGnD=O z`EzkP8Um0IN0=`6<;xdGjNR$CFr86XvNIa@=yG}*?m~VGqaPKb?Cfl@?FI{mauyU6 z*hh7b03<{o6_jd zuui=LxOC}~3!TB13Lz=Xv*U3|$!&-&?cCg)k{KBpaf2o#m}-I%Qi@1NxM7!_3#Ul> zV&zyQzngwd%ET*!A&fLw$Sbp{#q@_=uD$*x!zLHE&PnV^cv}$Tn>gxP30Fw~HqkYiqCm~17g$oxXOWF}$#B_v-6M#H( zBam)~#DoHZZ{Y=tj4zs;cDV zj56rix$V+h_Vn~fRRDl(^h)gcH8(d)4UPr?d(@xd;eo%rdu6Guy}kX; - 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);