settings_active_sessions.cpp 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "settings/settings_active_sessions.h"
  8. #include "apiwrap.h"
  9. #include "api/api_authorizations.h"
  10. #include "base/timer.h"
  11. #include "base/unixtime.h"
  12. #include "base/algorithm.h"
  13. #include "base/platform/base_platform_info.h"
  14. #include "boxes/self_destruction_box.h"
  15. #include "boxes/peer_lists_box.h"
  16. #include "ui/boxes/confirm_box.h"
  17. #include "lang/lang_keys.h"
  18. #include "main/main_session.h"
  19. #include "ui/widgets/buttons.h"
  20. #include "ui/widgets/fields/input_field.h"
  21. #include "ui/widgets/labels.h"
  22. #include "ui/widgets/scroll_area.h"
  23. #include "ui/wrap/slide_wrap.h"
  24. #include "ui/wrap/padding_wrap.h"
  25. #include "ui/wrap/vertical_layout.h"
  26. #include "ui/layers/generic_box.h"
  27. #include "ui/painter.h"
  28. #include "ui/vertical_list.h"
  29. #include "lottie/lottie_icon.h"
  30. #include "core/application.h"
  31. #include "core/core_settings.h"
  32. #include "window/window_session_controller.h"
  33. #include "styles/style_boxes.h"
  34. #include "styles/style_info.h"
  35. #include "styles/style_layers.h"
  36. #include "styles/style_settings.h"
  37. #include "styles/style_menu_icons.h"
  38. namespace {
  39. constexpr auto kShortPollTimeout = 60 * crl::time(1000);
  40. constexpr auto kMaxDeviceModelLength = 32;
  41. using EntryData = Api::Authorizations::Entry;
  42. enum class Type {
  43. Windows,
  44. Mac,
  45. Ubuntu,
  46. Linux,
  47. iPhone,
  48. iPad,
  49. Android,
  50. Web,
  51. Chrome,
  52. Edge,
  53. Firefox,
  54. Safari,
  55. Other,
  56. };
  57. class Row;
  58. class RowDelegate {
  59. public:
  60. virtual void rowUpdateRow(not_null<Row*> row) = 0;
  61. };
  62. class Row final : public PeerListRow {
  63. public:
  64. Row(not_null<RowDelegate*> delegate, const EntryData &data);
  65. void update(const EntryData &data);
  66. [[nodiscard]] EntryData data() const;
  67. QString generateName() override;
  68. QString generateShortName() override;
  69. PaintRoundImageCallback generatePaintUserpicCallback(
  70. bool forceRound) override;
  71. QSize rightActionSize() const override {
  72. return elementGeometry(2, 0).size();
  73. }
  74. QMargins rightActionMargins() const override {
  75. const auto rect = elementGeometry(2, 0);
  76. return QMargins(0, rect.y(), -(rect.x() + rect.width()), 0);
  77. }
  78. int elementsCount() const override;
  79. QRect elementGeometry(int element, int outerWidth) const override;
  80. bool elementDisabled(int element) const override;
  81. bool elementOnlySelect(int element) const override;
  82. void elementAddRipple(
  83. int element,
  84. QPoint point,
  85. Fn<void()> updateCallback) override;
  86. void elementsStopLastRipple() override;
  87. void elementsPaint(
  88. Painter &p,
  89. int outerWidth,
  90. bool selected,
  91. int selectedElement) override;
  92. private:
  93. const not_null<RowDelegate*> _delegate;
  94. Ui::Text::String _location;
  95. Type _type = Type::Other;
  96. EntryData _data;
  97. QImage _userpic;
  98. };
  99. void RenameBox(not_null<Ui::GenericBox*> box) {
  100. box->setTitle(tr::lng_settings_rename_device_title());
  101. const auto skip = st::defaultSubsectionTitlePadding.top();
  102. box->addRow(
  103. object_ptr<Ui::FlatLabel>(
  104. box,
  105. tr::lng_settings_device_name(),
  106. st::defaultSubsectionTitle),
  107. st::boxRowPadding + style::margins(0, skip, 0, 0));
  108. const auto name = box->addRow(
  109. object_ptr<Ui::InputField>(
  110. box,
  111. st::settingsDeviceName,
  112. rpl::single(Platform::DeviceModelPretty()),
  113. Core::App().settings().customDeviceModel()),
  114. st::boxRowPadding - style::margins(
  115. st::settingsDeviceName.textMargins.left(),
  116. 0,
  117. st::settingsDeviceName.textMargins.right(),
  118. 0));
  119. name->setMaxLength(kMaxDeviceModelLength);
  120. box->setFocusCallback([=] {
  121. name->setFocusFast();
  122. });
  123. const auto submit = [=] {
  124. const auto result = base::CleanAndSimplify(
  125. name->getLastText());
  126. box->closeBox();
  127. Core::App().settings().setCustomDeviceModel(result);
  128. Core::App().saveSettingsDelayed();
  129. };
  130. name->submits() | rpl::start_with_next(submit, name->lifetime());
  131. box->addButton(tr::lng_settings_save(), submit);
  132. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  133. }
  134. [[nodiscard]] QString LocationAndDate(const EntryData &entry) {
  135. return (entry.location.isEmpty() ? entry.ip : entry.location)
  136. + (entry.hash
  137. ? (QString::fromUtf8(" \xE2\x80\xA2 ") + entry.active)
  138. : QString());
  139. }
  140. [[nodiscard]] Type TypeFromEntry(const EntryData &entry) {
  141. const auto platform = entry.platform.toLower();
  142. const auto device = entry.name.toLower();
  143. const auto system = entry.system.toLower();
  144. const auto apiId = entry.apiId;
  145. const auto kDesktop = std::array{ 2040, 17349, 611335 };
  146. const auto kMac = std::array{ 2834 };
  147. const auto kAndroid
  148. = std::array{ 5, 6, 24, 1026, 1083, 2458, 2521, 21724 };
  149. const auto kiOS = std::array{ 1, 7, 10840, 16352 };
  150. const auto kWeb = std::array{ 2496, 739222, 1025907 };
  151. const auto detectBrowser = [&]() -> std::optional<Type> {
  152. if (device.contains("edg/")
  153. || device.contains("edgios/")
  154. || device.contains("edga/")) {
  155. return Type::Edge;
  156. } else if (device.contains("chrome")) {
  157. return Type::Chrome;
  158. } else if (device.contains("safari")) {
  159. return Type::Safari;
  160. } else if (device.contains("firefox")) {
  161. return Type::Firefox;
  162. }
  163. return {};
  164. };
  165. const auto detectDesktop = [&]() -> std::optional<Type> {
  166. if (platform.contains("windows") || system.contains("windows")) {
  167. return Type::Windows;
  168. } else if (platform.contains("macos") || system.contains("macos")) {
  169. return Type::Mac;
  170. } else if (platform.contains("ubuntu")
  171. || system.contains("ubuntu")
  172. || platform.contains("unity")
  173. || system.contains("unity")) {
  174. return Type::Ubuntu;
  175. } else if (platform.contains("linux") || system.contains("linux")) {
  176. return Type::Linux;
  177. }
  178. return {};
  179. };
  180. if (ranges::contains(kAndroid, apiId)) {
  181. return Type::Android;
  182. } else if (ranges::contains(kDesktop, apiId)) {
  183. return detectDesktop().value_or(Type::Linux);
  184. } else if (ranges::contains(kMac, apiId)) {
  185. return Type::Mac;
  186. } else if (ranges::contains(kWeb, apiId)) {
  187. return detectBrowser().value_or(Type::Web);
  188. } else if (device.contains("chromebook")) {
  189. return Type::Other;
  190. } else if (const auto browser = detectBrowser()) {
  191. return *browser;
  192. } else if (device.contains("iphone")) {
  193. return Type::iPhone;
  194. } else if (device.contains("ipad")) {
  195. return Type::iPad;
  196. } else if (ranges::contains(kiOS, apiId)) {
  197. return Type::iPhone;
  198. } else if (const auto desktop = detectDesktop()) {
  199. return *desktop;
  200. } else if (platform.contains("android") || system.contains("android")) {
  201. return Type::Android;
  202. } else if (platform.contains("ios") || system.contains("ios")) {
  203. return Type::iPhone;
  204. }
  205. return Type::Other;
  206. }
  207. [[nodiscard]] QBrush GradientForType(Type type, int size) {
  208. const auto colors = [&]() -> std::pair<style::color, style::color> {
  209. switch (type) {
  210. case Type::Windows:
  211. case Type::Mac:
  212. case Type::Other:
  213. // Blue.
  214. return { st::historyPeer4UserpicBg, st::historyPeer4UserpicBg2 };
  215. case Type::Ubuntu:
  216. // Orange.
  217. return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };
  218. case Type::Linux:
  219. // Purple.
  220. return { st::historyPeer5UserpicBg, st::historyPeer5UserpicBg2 };
  221. case Type::iPhone:
  222. case Type::iPad:
  223. // Sea.
  224. return { st::historyPeer7UserpicBg, st::historyPeer7UserpicBg2 };
  225. case Type::Android:
  226. // Green.
  227. return { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 };
  228. case Type::Web:
  229. case Type::Chrome:
  230. case Type::Edge:
  231. case Type::Firefox:
  232. case Type::Safari:
  233. // Pink.
  234. return { st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 };
  235. }
  236. Unexpected("Type in GradientForType.");
  237. }();
  238. auto gradient = QLinearGradient(0, 0, 0, size);
  239. gradient.setStops({
  240. { 0.0, colors.first->c },
  241. { 1.0, colors.second->c },
  242. });
  243. return QBrush(std::move(gradient));
  244. }
  245. [[nodiscard]] const style::icon &IconForType(Type type) {
  246. switch (type) {
  247. case Type::Windows: return st::sessionIconWindows;
  248. case Type::Mac: return st::sessionIconMac;
  249. case Type::Ubuntu: return st::sessionIconUbuntu;
  250. case Type::Linux: return st::sessionIconLinux;
  251. case Type::iPhone: return st::sessionIconiPhone;
  252. case Type::iPad: return st::sessionIconiPad;
  253. case Type::Android: return st::sessionIconAndroid;
  254. case Type::Web: return st::sessionIconWeb;
  255. case Type::Chrome: return st::sessionIconChrome;
  256. case Type::Edge: return st::sessionIconEdge;
  257. case Type::Firefox: return st::sessionIconFirefox;
  258. case Type::Safari: return st::sessionIconSafari;
  259. case Type::Other: return st::sessionIconOther;
  260. }
  261. Unexpected("Type in IconForType.");
  262. }
  263. [[nodiscard]] const style::icon *IconBigForType(Type type) {
  264. switch (type) {
  265. case Type::Web: return &st::sessionBigIconWeb;
  266. case Type::Other: return &st::sessionBigIconOther;
  267. }
  268. return nullptr;
  269. }
  270. [[nodiscard]] std::unique_ptr<Lottie::Icon> LottieForType(Type type) {
  271. if (IconBigForType(type)) {
  272. return nullptr;
  273. }
  274. const auto path = [&] {
  275. switch (type) {
  276. case Type::Windows: return "device_desktop_win";
  277. case Type::Mac: return "device_desktop_mac";
  278. case Type::Ubuntu: return "device_linux_ubuntu";
  279. case Type::Linux: return "device_linux";
  280. case Type::iPhone: return "device_phone_ios";
  281. case Type::iPad: return "device_tablet_ios";
  282. case Type::Android: return "device_phone_android";
  283. case Type::Chrome: return "device_web_chrome";
  284. case Type::Edge: return "device_web_edge";
  285. case Type::Firefox: return "device_web_firefox";
  286. case Type::Safari: return "device_web_safari";
  287. }
  288. Unexpected("Type in LottieForType.");
  289. }();
  290. const auto size = st::sessionBigLottieSize;
  291. return Lottie::MakeIcon({
  292. .path = u":/icons/settings/devices/"_q + path + u".lottie"_q,
  293. .sizeOverride = QSize(size, size),
  294. });
  295. }
  296. [[nodiscard]] QImage GenerateUserpic(Type type) {
  297. const auto size = st::sessionListItem.photoSize;
  298. const auto full = size * style::DevicePixelRatio();
  299. const auto rect = QRect(0, 0, size, size);
  300. auto result = QImage(full, full, QImage::Format_ARGB32_Premultiplied);
  301. result.fill(Qt::transparent);
  302. result.setDevicePixelRatio(style::DevicePixelRatio());
  303. auto p = QPainter(&result);
  304. auto hq = PainterHighQualityEnabler(p);
  305. p.setBrush(GradientForType(type, size));
  306. p.setPen(Qt::NoPen);
  307. p.drawEllipse(rect);
  308. IconForType(type).paintInCenter(p, rect);
  309. p.end();
  310. return result;
  311. }
  312. [[nodiscard]] not_null<Ui::RpWidget*> GenerateUserpicBig(
  313. not_null<Ui::RpWidget*> parent,
  314. rpl::producer<> shown,
  315. Type type) {
  316. const auto size = st::sessionBigUserpicSize;
  317. const auto full = size * style::DevicePixelRatio();
  318. const auto rect = QRect(0, 0, size, size);
  319. const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
  320. result->resize(rect.size());
  321. struct State {
  322. QImage background;
  323. std::unique_ptr<Lottie::Icon> lottie;
  324. QImage lottieFrame;
  325. QImage colorizedFrame;
  326. };
  327. const auto state = result->lifetime().make_state<State>();
  328. state->background = QImage(
  329. full,
  330. full,
  331. QImage::Format_ARGB32_Premultiplied);
  332. state->background.fill(Qt::transparent);
  333. state->background.setDevicePixelRatio(style::DevicePixelRatio());
  334. state->colorizedFrame = state->lottieFrame = state->background;
  335. auto p = QPainter(&state->background);
  336. auto hq = PainterHighQualityEnabler(p);
  337. p.setBrush(GradientForType(type, size));
  338. p.setPen(Qt::NoPen);
  339. p.drawEllipse(rect);
  340. if (const auto icon = IconBigForType(type)) {
  341. icon->paintInCenter(p, rect);
  342. }
  343. p.end();
  344. if ((state->lottie = LottieForType(type))) {
  345. std::move(
  346. shown
  347. ) | rpl::start_with_next([=] {
  348. state->lottie->animate(
  349. [=] { result->update(); },
  350. 0,
  351. state->lottie->framesCount() - 1);
  352. }, result->lifetime());
  353. }
  354. result->paintRequest(
  355. ) | rpl::start_with_next([=] {
  356. auto p = QPainter(result);
  357. p.drawImage(QPoint(0, 0), state->background);
  358. if (state->lottie) {
  359. state->lottieFrame.fill(Qt::black);
  360. auto q = QPainter(&state->lottieFrame);
  361. state->lottie->paintInCenter(q, result->rect());
  362. q.end();
  363. style::colorizeImage(
  364. state->lottieFrame,
  365. st::historyPeerUserpicFg->c,
  366. &state->colorizedFrame);
  367. p.drawImage(QPoint(0, 0), state->colorizedFrame);
  368. }
  369. }, result->lifetime());
  370. return result;
  371. }
  372. void SessionInfoBox(
  373. not_null<Ui::GenericBox*> box,
  374. const EntryData &data,
  375. Fn<void(uint64)> terminate) {
  376. box->setWidth(st::boxWideWidth);
  377. const auto shown = box->lifetime().make_state<rpl::event_stream<>>();
  378. box->setShowFinishedCallback([=] {
  379. shown->fire({});
  380. });
  381. const auto userpicWrap = box->addRow(
  382. object_ptr<Ui::FixedHeightWidget>(box, st::sessionBigUserpicSize),
  383. st::sessionBigCoverPadding);
  384. const auto big = GenerateUserpicBig(
  385. userpicWrap,
  386. shown->events(),
  387. TypeFromEntry(data));
  388. userpicWrap->sizeValue(
  389. ) | rpl::start_with_next([=](QSize size) {
  390. big->move((size.width() - big->width()) / 2, 0);
  391. }, userpicWrap->lifetime());
  392. const auto nameWrap = box->addRow(
  393. object_ptr<Ui::FixedHeightWidget>(
  394. box,
  395. st::sessionBigName.maxHeight));
  396. const auto name = Ui::CreateChild<Ui::FlatLabel>(
  397. nameWrap,
  398. rpl::single(data.name),
  399. st::sessionBigName);
  400. nameWrap->widthValue(
  401. ) | rpl::start_with_next([=](int width) {
  402. name->resizeToWidth(width);
  403. name->move((width - name->width()) / 2, 0);
  404. }, name->lifetime());
  405. const auto dateWrap = box->addRow(
  406. object_ptr<Ui::FixedHeightWidget>(
  407. box,
  408. st::sessionDateLabel.style.font->height),
  409. style::margins(0, 0, 0, st::sessionDateSkip));
  410. const auto date = Ui::CreateChild<Ui::FlatLabel>(
  411. dateWrap,
  412. rpl::single(
  413. langDateTimeFull(base::unixtime::parse(data.activeTime))),
  414. st::sessionDateLabel);
  415. rpl::combine(
  416. dateWrap->widthValue(),
  417. date->widthValue()
  418. ) | rpl::start_with_next([=](int outer, int inner) {
  419. date->move((outer - inner) / 2, 0);
  420. }, date->lifetime());
  421. using namespace Settings;
  422. const auto container = box->verticalLayout();
  423. Ui::AddDivider(container);
  424. Ui::AddSkip(container, st::sessionSubtitleSkip);
  425. Ui::AddSubsectionTitle(container, tr::lng_sessions_info());
  426. AddSessionInfoRow(
  427. container,
  428. tr::lng_sessions_application(),
  429. data.info,
  430. st::menuIconDevices);
  431. AddSessionInfoRow(
  432. container,
  433. tr::lng_sessions_system(),
  434. data.system,
  435. st::menuIconInfo);
  436. AddSessionInfoRow(
  437. container,
  438. tr::lng_sessions_ip(),
  439. data.ip,
  440. st::menuIconIpAddress);
  441. AddSessionInfoRow(
  442. container,
  443. tr::lng_sessions_location(),
  444. data.location,
  445. st::menuIconAddress);
  446. AddSkip(container, st::sessionValueSkip);
  447. if (!data.location.isEmpty()) {
  448. AddDividerText(container, tr::lng_sessions_location_about());
  449. }
  450. box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
  451. if (const auto hash = data.hash) {
  452. box->addLeftButton(tr::lng_sessions_terminate(), [=] {
  453. const auto weak = Ui::MakeWeak(box.get());
  454. terminate(hash);
  455. if (weak) {
  456. box->closeBox();
  457. }
  458. }, st::attentionBoxButton);
  459. }
  460. }
  461. Row::Row(not_null<RowDelegate*> delegate, const EntryData &data)
  462. : PeerListRow(data.hash)
  463. , _delegate(delegate)
  464. , _location(st::defaultTextStyle, LocationAndDate(data))
  465. , _type(TypeFromEntry(data))
  466. , _data(data)
  467. , _userpic(GenerateUserpic(_type)) {
  468. setCustomStatus(_data.info);
  469. }
  470. void Row::update(const EntryData &data) {
  471. _data = data;
  472. setCustomStatus(_data.info);
  473. refreshName(st::sessionListItem);
  474. _location.setText(st::defaultTextStyle, LocationAndDate(_data));
  475. _type = TypeFromEntry(_data);
  476. _userpic = GenerateUserpic(_type);
  477. _delegate->rowUpdateRow(this);
  478. }
  479. EntryData Row::data() const {
  480. return _data;
  481. }
  482. QString Row::generateName() {
  483. return _data.name;
  484. }
  485. QString Row::generateShortName() {
  486. return generateName();
  487. }
  488. PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
  489. return [=](
  490. QPainter &p,
  491. int x,
  492. int y,
  493. int outerWidth,
  494. int size) {
  495. p.drawImage(x, y, _userpic);
  496. };
  497. }
  498. int Row::elementsCount() const {
  499. return 2;
  500. }
  501. QRect Row::elementGeometry(int element, int outerWidth) const {
  502. switch (element) {
  503. case 1: {
  504. return QRect(
  505. st::sessionListItem.namePosition.x(),
  506. st::sessionLocationTop,
  507. outerWidth,
  508. st::normalFont->height);
  509. } break;
  510. case 2: {
  511. const auto size = QSize(
  512. st::sessionTerminate.width,
  513. st::sessionTerminate.height);
  514. const auto right = st::sessionTerminateSkip;
  515. const auto top = st::sessionTerminateTop;
  516. const auto left = outerWidth - right - size.width();
  517. return QRect(QPoint(left, top), size);
  518. } break;
  519. }
  520. return QRect();
  521. }
  522. bool Row::elementDisabled(int element) const {
  523. return !id() || (element == 1);
  524. }
  525. bool Row::elementOnlySelect(int element) const {
  526. return false;
  527. }
  528. void Row::elementAddRipple(
  529. int element,
  530. QPoint point,
  531. Fn<void()> updateCallback) {
  532. }
  533. void Row::elementsStopLastRipple() {
  534. }
  535. void Row::elementsPaint(
  536. Painter &p,
  537. int outerWidth,
  538. bool selected,
  539. int selectedElement) {
  540. if (id()) {
  541. const auto geometry = elementGeometry(2, outerWidth);
  542. const auto position = geometry.topLeft()
  543. + st::sessionTerminate.iconPosition;
  544. const auto &icon = (selectedElement == 2)
  545. ? st::sessionTerminate.iconOver
  546. : st::sessionTerminate.icon;
  547. icon.paint(p, position.x(), position.y(), outerWidth);
  548. }
  549. p.setFont(st::normalFont);
  550. p.setPen(st::sessionInfoFg);
  551. const auto locationLeft = st::sessionListItem.namePosition.x();
  552. const auto available = outerWidth - locationLeft;
  553. _location.drawLeftElided(
  554. p,
  555. locationLeft,
  556. st::sessionLocationTop,
  557. available,
  558. outerWidth);
  559. }
  560. class SessionsContent : public Ui::RpWidget {
  561. public:
  562. SessionsContent(
  563. QWidget*,
  564. not_null<Window::SessionController*> controller);
  565. void setupContent();
  566. protected:
  567. void resizeEvent(QResizeEvent *e) override;
  568. void paintEvent(QPaintEvent *e) override;
  569. private:
  570. struct Full {
  571. EntryData current;
  572. std::vector<EntryData> incomplete;
  573. std::vector<EntryData> list;
  574. };
  575. class Inner;
  576. class ListController;
  577. void shortPollSessions();
  578. void parse(const Api::Authorizations::List &list);
  579. void terminate(Fn<void()> terminateRequest, QString message);
  580. void terminateOne(uint64 hash);
  581. void terminateAll();
  582. const not_null<Window::SessionController*> _controller;
  583. const not_null<Api::Authorizations*> _authorizations;
  584. rpl::variable<bool> _loading = false;
  585. Full _data;
  586. object_ptr<Inner> _inner;
  587. QPointer<Ui::BoxContent> _terminateBox;
  588. base::Timer _shortPollTimer;
  589. };
  590. class SessionsContent::ListController final
  591. : public PeerListController
  592. , public RowDelegate
  593. , public base::has_weak_ptr {
  594. public:
  595. explicit ListController(not_null<Main::Session*> session);
  596. Main::Session &session() const override;
  597. void prepare() override;
  598. void rowClicked(not_null<PeerListRow*> row) override;
  599. void rowElementClicked(not_null<PeerListRow*> row, int element) override;
  600. void rowUpdateRow(not_null<Row*> row) override;
  601. void showData(gsl::span<const EntryData> items);
  602. rpl::producer<int> itemsCount() const;
  603. rpl::producer<uint64> terminateRequests() const;
  604. [[nodiscard]] rpl::producer<EntryData> showRequests() const;
  605. [[nodiscard]] static std::unique_ptr<ListController> Add(
  606. not_null<Ui::VerticalLayout*> container,
  607. not_null<Main::Session*> session,
  608. style::margins margins = {});
  609. private:
  610. const not_null<Main::Session*> _session;
  611. rpl::event_stream<uint64> _terminateRequests;
  612. rpl::event_stream<int> _itemsCount;
  613. rpl::event_stream<EntryData> _showRequests;
  614. };
  615. class SessionsContent::Inner : public Ui::RpWidget {
  616. public:
  617. Inner(
  618. QWidget *parent,
  619. not_null<Window::SessionController*> controller,
  620. rpl::producer<int> ttlDays);
  621. void showData(const Full &data);
  622. [[nodiscard]] rpl::producer<EntryData> showRequests() const;
  623. [[nodiscard]] rpl::producer<uint64> terminateOne() const;
  624. [[nodiscard]] rpl::producer<> terminateAll() const;
  625. private:
  626. void setupContent();
  627. const not_null<Window::SessionController*> _controller;
  628. std::unique_ptr<ListController> _current;
  629. QPointer<Ui::SettingsButton> _terminateAll;
  630. std::unique_ptr<ListController> _incomplete;
  631. std::unique_ptr<ListController> _list;
  632. rpl::variable<int> _ttlDays;
  633. };
  634. //, location(st::sessionInfoStyle, LocationAndDate(entry))
  635. SessionsContent::SessionsContent(
  636. QWidget*,
  637. not_null<Window::SessionController*> controller)
  638. : _controller(controller)
  639. , _authorizations(&controller->session().api().authorizations())
  640. , _inner(this, controller, _authorizations->ttlDays())
  641. , _shortPollTimer([=] { shortPollSessions(); }) {
  642. }
  643. void SessionsContent::setupContent() {
  644. _inner->resize(width(), st::noContactsHeight);
  645. _inner->heightValue(
  646. ) | rpl::distinct_until_changed(
  647. ) | rpl::start_with_next([=](int height) {
  648. resize(width(), height);
  649. }, _inner->lifetime());
  650. _inner->showRequests(
  651. ) | rpl::start_with_next([=](const EntryData &data) {
  652. _controller->show(Box(
  653. SessionInfoBox,
  654. data,
  655. [=](uint64 hash) { terminateOne(hash); }));
  656. }, lifetime());
  657. _inner->terminateOne(
  658. ) | rpl::start_with_next([=](uint64 hash) {
  659. terminateOne(hash);
  660. }, lifetime());
  661. _inner->terminateAll(
  662. ) | rpl::start_with_next([=] {
  663. terminateAll();
  664. }, lifetime());
  665. _loading.changes(
  666. ) | rpl::start_with_next([=](bool value) {
  667. _inner->setVisible(!value);
  668. }, lifetime());
  669. _authorizations->listValue(
  670. ) | rpl::start_with_next([=](const Api::Authorizations::List &list) {
  671. parse(list);
  672. }, lifetime());
  673. _loading = true;
  674. shortPollSessions();
  675. }
  676. void SessionsContent::parse(const Api::Authorizations::List &list) {
  677. if (list.empty()) {
  678. return;
  679. }
  680. _data = Full();
  681. for (const auto &auth : list) {
  682. if (!auth.hash) {
  683. _data.current = auth;
  684. } else if (auth.incomplete) {
  685. _data.incomplete.push_back(auth);
  686. } else {
  687. _data.list.push_back(auth);
  688. }
  689. }
  690. _loading = false;
  691. ranges::sort(_data.list, std::greater<>(), &EntryData::activeTime);
  692. ranges::sort(_data.incomplete, std::greater<>(), &EntryData::activeTime);
  693. _inner->showData(_data);
  694. _shortPollTimer.callOnce(kShortPollTimeout);
  695. }
  696. void SessionsContent::resizeEvent(QResizeEvent *e) {
  697. RpWidget::resizeEvent(e);
  698. _inner->resize(width(), _inner->height());
  699. }
  700. void SessionsContent::paintEvent(QPaintEvent *e) {
  701. RpWidget::paintEvent(e);
  702. Painter p(this);
  703. if (_loading.current()) {
  704. p.setFont(st::noContactsFont);
  705. p.setPen(st::noContactsColor);
  706. p.drawText(
  707. QRect(0, 0, width(), st::noContactsHeight),
  708. tr::lng_contacts_loading(tr::now),
  709. style::al_center);
  710. }
  711. }
  712. void SessionsContent::shortPollSessions() {
  713. const auto left = kShortPollTimeout
  714. - (crl::now() - _authorizations->lastReceivedTime());
  715. if (left > 0) {
  716. parse(_authorizations->list());
  717. _shortPollTimer.cancel();
  718. _shortPollTimer.callOnce(left);
  719. } else {
  720. _authorizations->reload();
  721. }
  722. update();
  723. }
  724. void SessionsContent::terminate(Fn<void()> terminateRequest, QString message) {
  725. if (_terminateBox) {
  726. _terminateBox->deleteLater();
  727. }
  728. const auto callback = crl::guard(this, [=] {
  729. if (_terminateBox) {
  730. _terminateBox->closeBox();
  731. _terminateBox = nullptr;
  732. }
  733. terminateRequest();
  734. });
  735. auto box = Ui::MakeConfirmBox({
  736. .text = message,
  737. .confirmed = callback,
  738. .confirmText = tr::lng_settings_reset_button(),
  739. .confirmStyle = &st::attentionBoxButton,
  740. });
  741. _terminateBox = Ui::MakeWeak(box.data());
  742. _controller->show(std::move(box));
  743. }
  744. void SessionsContent::terminateOne(uint64 hash) {
  745. const auto weak = Ui::MakeWeak(this);
  746. auto callback = [=] {
  747. auto done = crl::guard(weak, [=](const MTPBool &result) {
  748. if (mtpIsFalse(result)) {
  749. return;
  750. }
  751. const auto removeByHash = [&](std::vector<EntryData> &list) {
  752. list.erase(
  753. ranges::remove(
  754. list,
  755. hash,
  756. [](const EntryData &entry) { return entry.hash; }),
  757. end(list));
  758. };
  759. removeByHash(_data.incomplete);
  760. removeByHash(_data.list);
  761. _inner->showData(_data);
  762. });
  763. auto fail = crl::guard(weak, [=](const MTP::Error &error) {
  764. });
  765. _authorizations->requestTerminate(
  766. std::move(done),
  767. std::move(fail),
  768. hash);
  769. };
  770. terminate(std::move(callback), tr::lng_settings_reset_one_sure(tr::now));
  771. }
  772. void SessionsContent::terminateAll() {
  773. const auto weak = Ui::MakeWeak(this);
  774. auto callback = [=] {
  775. const auto reset = crl::guard(weak, [=] {
  776. _authorizations->cancelCurrentRequest();
  777. _authorizations->reload();
  778. });
  779. _authorizations->requestTerminate(
  780. [=](const MTPBool &result) { reset(); },
  781. [=](const MTP::Error &result) { reset(); });
  782. _loading = true;
  783. };
  784. terminate(std::move(callback), tr::lng_settings_reset_sure(tr::now));
  785. }
  786. SessionsContent::Inner::Inner(
  787. QWidget *parent,
  788. not_null<Window::SessionController*> controller,
  789. rpl::producer<int> ttlDays)
  790. : RpWidget(parent)
  791. , _controller(controller)
  792. , _ttlDays(std::move(ttlDays)) {
  793. setupContent();
  794. }
  795. void SessionsContent::Inner::setupContent() {
  796. using namespace Settings;
  797. using namespace rpl::mappers;
  798. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  799. const auto header = AddSubsectionTitle(
  800. content,
  801. tr::lng_sessions_header());
  802. const auto rename = Ui::CreateChild<Ui::LinkButton>(
  803. content,
  804. tr::lng_settings_rename_device(tr::now),
  805. st::defaultLinkButton);
  806. rpl::combine(
  807. content->sizeValue(),
  808. header->positionValue()
  809. ) | rpl::start_with_next([=](QSize outer, QPoint position) {
  810. const auto x = st::sessionTerminateSkip
  811. + st::sessionTerminate.iconPosition.x();
  812. const auto y = st::defaultSubsectionTitlePadding.top()
  813. + st::defaultSubsectionTitle.style.font->ascent
  814. - st::defaultLinkButton.font->ascent;
  815. rename->moveToRight(x, y, outer.width());
  816. }, rename->lifetime());
  817. rename->setClickedCallback([=] {
  818. _controller->show(Box(RenameBox));
  819. });
  820. const auto session = &_controller->session();
  821. _current = ListController::Add(
  822. content,
  823. session,
  824. style::margins{ 0, 0, 0, st::sessionCurrentSkip });
  825. const auto terminateWrap = content->add(
  826. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  827. content,
  828. object_ptr<Ui::VerticalLayout>(content)))->setDuration(0);
  829. const auto terminateInner = terminateWrap->entity();
  830. _terminateAll = terminateInner->add(
  831. CreateButtonWithIcon(
  832. terminateInner,
  833. tr::lng_sessions_terminate_all(),
  834. st::infoBlockButton,
  835. { .icon = &st::infoIconBlock }));
  836. AddSkip(terminateInner);
  837. AddDividerText(terminateInner, tr::lng_sessions_terminate_all_about());
  838. const auto incompleteWrap = content->add(
  839. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  840. content,
  841. object_ptr<Ui::VerticalLayout>(content)))->setDuration(0);
  842. const auto incompleteInner = incompleteWrap->entity();
  843. AddSkip(incompleteInner, st::sessionSubtitleSkip);
  844. AddSubsectionTitle(incompleteInner, tr::lng_sessions_incomplete());
  845. _incomplete = ListController::Add(incompleteInner, session);
  846. AddSkip(incompleteInner);
  847. AddDividerText(incompleteInner, tr::lng_sessions_incomplete_about());
  848. const auto listWrap = content->add(
  849. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  850. content,
  851. object_ptr<Ui::VerticalLayout>(content)))->setDuration(0);
  852. const auto listInner = listWrap->entity();
  853. AddSkip(listInner, st::sessionSubtitleSkip);
  854. AddSubsectionTitle(listInner, tr::lng_sessions_other_header());
  855. _list = ListController::Add(listInner, session);
  856. AddSkip(listInner);
  857. AddDividerText(listInner, tr::lng_sessions_about_apps());
  858. const auto ttlWrap = content->add(
  859. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  860. content,
  861. object_ptr<Ui::VerticalLayout>(content)))->setDuration(0);
  862. const auto ttlInner = ttlWrap->entity();
  863. AddSkip(ttlInner, st::sessionSubtitleSkip);
  864. AddSubsectionTitle(ttlInner, tr::lng_settings_terminate_title());
  865. AddButtonWithLabel(
  866. ttlInner,
  867. tr::lng_settings_terminate_if(),
  868. _ttlDays.value() | rpl::map(SelfDestructionBox::DaysLabel),
  869. st::settingsButtonNoIcon
  870. )->addClickHandler([=] {
  871. _controller->show(Box<SelfDestructionBox>(
  872. &_controller->session(),
  873. SelfDestructionBox::Type::Sessions,
  874. _ttlDays.value()));
  875. });
  876. AddSkip(ttlInner);
  877. const auto placeholder = content->add(
  878. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  879. content,
  880. object_ptr<Ui::FlatLabel>(
  881. content,
  882. tr::lng_sessions_other_desc(),
  883. st::boxDividerLabel),
  884. st::defaultBoxDividerLabelPadding))->setDuration(0);
  885. terminateWrap->toggleOn(
  886. rpl::combine(
  887. _incomplete->itemsCount(),
  888. _list->itemsCount(),
  889. (_1 + _2) > 0));
  890. incompleteWrap->toggleOn(_incomplete->itemsCount() | rpl::map(_1 > 0));
  891. listWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0));
  892. ttlWrap->toggleOn(_list->itemsCount() | rpl::map(_1 > 0));
  893. placeholder->toggleOn(_list->itemsCount() | rpl::map(_1 == 0));
  894. Ui::ResizeFitChild(this, content);
  895. }
  896. void SessionsContent::Inner::showData(const Full &data) {
  897. _current->showData({ &data.current, &data.current + 1 });
  898. _list->showData(data.list);
  899. _incomplete->showData(data.incomplete);
  900. }
  901. rpl::producer<> SessionsContent::Inner::terminateAll() const {
  902. return _terminateAll->clicks() | rpl::to_empty;
  903. }
  904. rpl::producer<uint64> SessionsContent::Inner::terminateOne() const {
  905. return rpl::merge(
  906. _incomplete->terminateRequests(),
  907. _list->terminateRequests());
  908. }
  909. rpl::producer<EntryData> SessionsContent::Inner::showRequests() const {
  910. return rpl::merge(
  911. _current->showRequests(),
  912. _incomplete->showRequests(),
  913. _list->showRequests());
  914. }
  915. SessionsContent::ListController::ListController(
  916. not_null<Main::Session*> session)
  917. : _session(session) {
  918. }
  919. Main::Session &SessionsContent::ListController::session() const {
  920. return *_session;
  921. }
  922. void SessionsContent::ListController::prepare() {
  923. }
  924. void SessionsContent::ListController::rowClicked(
  925. not_null<PeerListRow*> row) {
  926. _showRequests.fire_copy(static_cast<Row*>(row.get())->data());
  927. }
  928. void SessionsContent::ListController::rowElementClicked(
  929. not_null<PeerListRow*> row,
  930. int element) {
  931. if (element == 2) {
  932. if (const auto hash = static_cast<Row*>(row.get())->data().hash) {
  933. _terminateRequests.fire_copy(hash);
  934. }
  935. }
  936. }
  937. void SessionsContent::ListController::rowUpdateRow(not_null<Row*> row) {
  938. delegate()->peerListUpdateRow(row);
  939. }
  940. void SessionsContent::ListController::showData(
  941. gsl::span<const EntryData> items) {
  942. auto index = 0;
  943. auto positions = base::flat_map<uint64, int>();
  944. positions.reserve(items.size());
  945. for (const auto &entry : items) {
  946. const auto id = entry.hash;
  947. positions.emplace(id, index++);
  948. if (const auto row = delegate()->peerListFindRow(id)) {
  949. static_cast<Row*>(row)->update(entry);
  950. } else {
  951. delegate()->peerListAppendRow(
  952. std::make_unique<Row>(this, entry));
  953. }
  954. }
  955. for (auto i = 0; i != delegate()->peerListFullRowsCount();) {
  956. const auto row = delegate()->peerListRowAt(i);
  957. if (positions.contains(row->id())) {
  958. ++i;
  959. continue;
  960. }
  961. delegate()->peerListRemoveRow(row);
  962. }
  963. delegate()->peerListSortRows([&](
  964. const PeerListRow &a,
  965. const PeerListRow &b) {
  966. return positions[a.id()] < positions[b.id()];
  967. });
  968. delegate()->peerListRefreshRows();
  969. _itemsCount.fire(delegate()->peerListFullRowsCount());
  970. }
  971. rpl::producer<int> SessionsContent::ListController::itemsCount() const {
  972. return _itemsCount.events_starting_with(
  973. delegate()->peerListFullRowsCount());
  974. }
  975. rpl::producer<uint64> SessionsContent::ListController::terminateRequests() const {
  976. return _terminateRequests.events();
  977. }
  978. rpl::producer<EntryData> SessionsContent::ListController::showRequests() const {
  979. return _showRequests.events();
  980. }
  981. auto SessionsContent::ListController::Add(
  982. not_null<Ui::VerticalLayout*> container,
  983. not_null<Main::Session*> session,
  984. style::margins margins)
  985. -> std::unique_ptr<ListController> {
  986. auto &lifetime = container->lifetime();
  987. const auto delegate = lifetime.make_state<
  988. PeerListContentDelegateSimple
  989. >();
  990. auto controller = std::make_unique<ListController>(session);
  991. controller->setStyleOverrides(&st::sessionList);
  992. const auto content = container->add(
  993. object_ptr<PeerListContent>(
  994. container,
  995. controller.get()),
  996. margins);
  997. delegate->setContent(content);
  998. controller->setDelegate(delegate);
  999. return controller;
  1000. }
  1001. } // namespace
  1002. namespace Settings {
  1003. Sessions::Sessions(
  1004. QWidget *parent,
  1005. not_null<Window::SessionController*> controller)
  1006. : Section(parent) {
  1007. setupContent(controller);
  1008. }
  1009. rpl::producer<QString> Sessions::title() {
  1010. return tr::lng_settings_sessions_title();
  1011. }
  1012. void Sessions::setupContent(not_null<Window::SessionController*> controller) {
  1013. const auto container = Ui::CreateChild<Ui::VerticalLayout>(this);
  1014. AddSkip(container, st::settingsPrivacySkip);
  1015. const auto content = container->add(
  1016. object_ptr<SessionsContent>(container, controller));
  1017. content->setupContent();
  1018. Ui::ResizeFitChild(this, container);
  1019. }
  1020. void AddSessionInfoRow(
  1021. not_null<Ui::VerticalLayout*> container,
  1022. rpl::producer<QString> label,
  1023. const QString &value,
  1024. const style::icon &icon) {
  1025. if (value.isEmpty()) {
  1026. return;
  1027. }
  1028. const auto text = container->add(
  1029. object_ptr<Ui::FlatLabel>(
  1030. container,
  1031. rpl::single(value),
  1032. st::boxLabel),
  1033. st::boxRowPadding + st::sessionValuePadding);
  1034. const auto left = st::sessionValuePadding.left();
  1035. container->add(
  1036. object_ptr<Ui::FlatLabel>(
  1037. container,
  1038. std::move(label),
  1039. st::sessionValueLabel),
  1040. (st::boxRowPadding
  1041. + style::margins{ left, 0, 0, st::sessionValueSkip }));
  1042. const auto widget = Ui::CreateChild<Ui::RpWidget>(container.get());
  1043. widget->resize(icon.size());
  1044. text->topValue() | rpl::start_with_next([=](int top) {
  1045. widget->move(st::sessionValueIconPosition + QPoint(0, top));
  1046. }, widget->lifetime());
  1047. widget->paintRequest() | rpl::start_with_next([=, &icon] {
  1048. auto p = QPainter(widget);
  1049. icon.paintInCenter(p, widget->rect());
  1050. }, widget->lifetime());
  1051. }
  1052. } // namespace Settings