settings_information.cpp 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  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_information.h"
  8. #include "ui/wrap/vertical_layout.h"
  9. #include "ui/wrap/vertical_layout_reorder.h"
  10. #include "ui/wrap/padding_wrap.h"
  11. #include "ui/wrap/slide_wrap.h"
  12. #include "ui/widgets/labels.h"
  13. #include "ui/widgets/buttons.h"
  14. #include "ui/widgets/fields/input_field.h"
  15. #include "ui/widgets/popup_menu.h"
  16. #include "ui/widgets/box_content_divider.h"
  17. #include "ui/widgets/menu/menu_add_action_callback_factory.h"
  18. #include "ui/boxes/confirm_box.h"
  19. #include "ui/controls/userpic_button.h"
  20. #include "ui/text/text_utilities.h"
  21. #include "ui/delayed_activation.h"
  22. #include "ui/painter.h"
  23. #include "ui/vertical_list.h"
  24. #include "ui/unread_badge_paint.h"
  25. #include "ui/ui_utility.h"
  26. #include "core/application.h"
  27. #include "core/click_handler_types.h"
  28. #include "core/core_settings.h"
  29. #include "chat_helpers/emoji_suggestions_widget.h"
  30. #include "boxes/add_contact_box.h"
  31. #include "boxes/premium_limits_box.h"
  32. #include "boxes/username_box.h"
  33. #include "data/data_session.h"
  34. #include "data/data_user.h"
  35. #include "data/data_peer_values.h"
  36. #include "data/data_changes.h"
  37. #include "data/data_channel.h"
  38. #include "data/data_premium_limits.h"
  39. #include "info/profile/info_profile_values.h"
  40. #include "info/profile/info_profile_badge.h"
  41. #include "lang/lang_keys.h"
  42. #include "main/main_account.h"
  43. #include "main/main_session.h"
  44. #include "main/main_domain.h"
  45. #include "mtproto/mtproto_dc_options.h"
  46. #include "window/window_session_controller.h"
  47. #include "window/window_controller.h"
  48. #include "window/window_peer_menu.h"
  49. #include "apiwrap.h"
  50. #include "api/api_peer_photo.h"
  51. #include "api/api_user_names.h"
  52. #include "api/api_user_privacy.h"
  53. #include "base/call_delayed.h"
  54. #include "base/options.h"
  55. #include "base/unixtime.h"
  56. #include "base/random.h"
  57. #include "styles/style_chat.h" // popupMenuExpandedSeparator
  58. #include "styles/style_dialogs.h" // dialogsPremiumIcon
  59. #include "styles/style_layers.h"
  60. #include "styles/style_settings.h"
  61. #include "styles/style_menu_icons.h"
  62. #include "styles/style_window.h"
  63. #include <QtGui/QGuiApplication>
  64. #include <QtCore/QBuffer>
  65. namespace Settings {
  66. namespace {
  67. constexpr auto kSaveBioTimeout = 1000;
  68. constexpr auto kPlayStatusLimit = 2;
  69. class ComposedBadge final : public Ui::RpWidget {
  70. public:
  71. ComposedBadge(
  72. not_null<Ui::RpWidget*> parent,
  73. not_null<Ui::SettingsButton*> button,
  74. not_null<Main::Session*> session,
  75. rpl::producer<QString> &&text,
  76. bool hasUnread,
  77. Fn<bool()> animationPaused);
  78. private:
  79. rpl::variable<QString> _text;
  80. rpl::event_stream<int> _unreadWidth;
  81. rpl::event_stream<int> _premiumWidth;
  82. QPointer<Ui::RpWidget> _unread;
  83. Info::Profile::Badge _badge;
  84. };
  85. ComposedBadge::ComposedBadge(
  86. not_null<Ui::RpWidget*> parent,
  87. not_null<Ui::SettingsButton*> button,
  88. not_null<Main::Session*> session,
  89. rpl::producer<QString> &&text,
  90. bool hasUnread,
  91. Fn<bool()> animationPaused)
  92. : Ui::RpWidget(parent)
  93. , _text(std::move(text))
  94. , _badge(
  95. this,
  96. st::settingsInfoPeerBadge,
  97. session,
  98. Info::Profile::BadgeContentForPeer(session->user()),
  99. nullptr,
  100. std::move(animationPaused),
  101. kPlayStatusLimit,
  102. Info::Profile::BadgeType::Premium) {
  103. if (hasUnread) {
  104. _unread = CreateUnread(this, rpl::single(
  105. rpl::empty
  106. ) | rpl::then(
  107. session->data().unreadBadgeChanges()
  108. ) | rpl::map([=] {
  109. auto &owner = session->data();
  110. return Badge::UnreadBadge{
  111. owner.unreadBadge(),
  112. owner.unreadBadgeMuted(),
  113. };
  114. }));
  115. rpl::combine(
  116. _unread->shownValue(),
  117. _unread->widthValue()
  118. ) | rpl::map([=](bool shown, int width) {
  119. return shown ? width : 0;
  120. }) | rpl::start_to_stream(_unreadWidth, _unread->lifetime());
  121. }
  122. _badge.updated(
  123. ) | rpl::start_with_next([=] {
  124. if (const auto button = _badge.widget()) {
  125. button->widthValue(
  126. ) | rpl::start_to_stream(_premiumWidth, button->lifetime());
  127. } else {
  128. _premiumWidth.fire(0);
  129. }
  130. }, lifetime());
  131. auto textWidth = _text.value() | rpl::map([=] {
  132. return button->fullTextWidth();
  133. });
  134. rpl::combine(
  135. _unreadWidth.events_starting_with(_unread ? _unread->width() : 0),
  136. _premiumWidth.events_starting_with(_badge.widget()
  137. ? _badge.widget()->width()
  138. : 0),
  139. std::move(textWidth),
  140. button->sizeValue()
  141. ) | rpl::start_with_next([=](
  142. int unreadWidth,
  143. int premiumWidth,
  144. int textWidth,
  145. const QSize &buttonSize) {
  146. const auto &st = button->st();
  147. const auto skip = st.style.font->spacew;
  148. const auto textRightPosition = st.padding.left()
  149. + textWidth
  150. + skip;
  151. const auto minWidth = unreadWidth + premiumWidth + skip;
  152. const auto maxTextWidth = buttonSize.width()
  153. - minWidth
  154. - st.padding.right();
  155. const auto finalTextRight = std::min(textRightPosition, maxTextWidth);
  156. resize(
  157. buttonSize.width() - st.padding.right() - finalTextRight,
  158. buttonSize.height());
  159. _badge.move(
  160. 0,
  161. st.padding.top(),
  162. buttonSize.height() - st.padding.top());
  163. if (_unread) {
  164. _unread->moveToRight(
  165. 0,
  166. (buttonSize.height() - _unread->height()) / 2);
  167. }
  168. }, lifetime());
  169. }
  170. class AccountsList final {
  171. public:
  172. AccountsList(
  173. not_null<Ui::VerticalLayout*> container,
  174. not_null<Window::SessionController*> controller);
  175. [[nodiscard]] rpl::producer<> closeRequests() const;
  176. private:
  177. void setup();
  178. [[nodiscard]] not_null<Ui::SlideWrap<Ui::SettingsButton>*> setupAdd();
  179. void rebuild();
  180. const not_null<Window::SessionController*> _controller;
  181. const not_null<Ui::VerticalLayout*> _outer;
  182. int _outerIndex = 0;
  183. Ui::SlideWrap<Ui::SettingsButton> *_addAccount = nullptr;
  184. base::flat_map<
  185. not_null<Main::Account*>,
  186. base::unique_qptr<Ui::SettingsButton>> _watched;
  187. base::unique_qptr<Ui::PopupMenu> _contextMenu;
  188. std::unique_ptr<Ui::VerticalLayoutReorder> _reorder;
  189. int _reordering = 0;
  190. rpl::event_stream<> _closeRequests;
  191. base::binary_guard _accountSwitchGuard;
  192. };
  193. [[nodiscard]] rpl::producer<TextWithEntities> StatusValue(
  194. not_null<UserData*> user) {
  195. return [=](auto consumer) {
  196. auto lifetime = rpl::lifetime();
  197. const auto timer = lifetime.make_state<base::Timer>();
  198. const auto push = [=] {
  199. const auto now = base::unixtime::now();
  200. consumer.put_next(Data::OnlineTextActive(user, now)
  201. ? Ui::Text::Link(Data::OnlineText(user, now))
  202. : Ui::Text::WithEntities(Data::OnlineText(user, now)));
  203. timer->callOnce(Data::OnlineChangeTimeout(user, now));
  204. };
  205. timer->setCallback(push);
  206. user->session().changes().peerFlagsValue(
  207. user,
  208. Data::PeerUpdate::Flag::OnlineStatus
  209. ) | rpl::start_with_next(push, lifetime);
  210. return lifetime;
  211. };
  212. }
  213. void SetupPhoto(
  214. not_null<Ui::VerticalLayout*> container,
  215. not_null<Window::SessionController*> controller,
  216. not_null<UserData*> self) {
  217. const auto wrap = container->add(object_ptr<Ui::FixedHeightWidget>(
  218. container,
  219. st::settingsInfoPhotoHeight));
  220. const auto photo = Ui::CreateChild<Ui::UserpicButton>(
  221. wrap,
  222. controller,
  223. self,
  224. Ui::UserpicButton::Role::OpenPhoto,
  225. Ui::UserpicButton::Source::PeerPhoto,
  226. st::settingsInfoPhoto);
  227. const auto upload = CreateUploadSubButton(wrap, controller);
  228. upload->chosenImages(
  229. ) | rpl::start_with_next([=](Ui::UserpicButton::ChosenImage &&chosen) {
  230. auto &image = chosen.image;
  231. UpdatePhotoLocally(self, image);
  232. photo->showCustom(base::duplicate(image));
  233. self->session().api().peerPhoto().upload(
  234. self,
  235. {
  236. std::move(image),
  237. chosen.markup.documentId,
  238. chosen.markup.colors,
  239. });
  240. }, upload->lifetime());
  241. const auto name = Ui::CreateChild<Ui::FlatLabel>(
  242. wrap,
  243. Info::Profile::NameValue(self),
  244. st::settingsCoverName);
  245. const auto status = Ui::CreateChild<Ui::FlatLabel>(
  246. wrap,
  247. StatusValue(self),
  248. st::settingsCoverStatus);
  249. status->setAttribute(Qt::WA_TransparentForMouseEvents);
  250. rpl::combine(
  251. wrap->widthValue(),
  252. photo->widthValue(),
  253. Info::Profile::NameValue(self),
  254. status->widthValue()
  255. ) | rpl::start_with_next([=](
  256. int max,
  257. int photoWidth,
  258. const QString&,
  259. int statusWidth) {
  260. photo->moveToLeft(
  261. (max - photoWidth) / 2,
  262. st::settingsInfoPhotoTop);
  263. upload->moveToLeft(
  264. ((max - photoWidth) / 2
  265. + photoWidth
  266. - upload->width()
  267. + st::settingsInfoUploadLeft),
  268. photo->y() + photo->height() - upload->height());
  269. const auto skip = st::settingsButton.iconLeft;
  270. name->resizeToNaturalWidth(max - 2 * skip);
  271. name->moveToLeft(
  272. (max - name->width()) / 2,
  273. (photo->y() + photo->height() + st::settingsInfoPhotoSkip));
  274. status->moveToLeft(
  275. (max - statusWidth) / 2,
  276. (name->y() + name->height() + st::settingsInfoNameSkip));
  277. }, photo->lifetime());
  278. }
  279. void ShowMenu(
  280. QWidget *parent,
  281. const QString &copyButton,
  282. const QString &text) {
  283. const auto menu = Ui::CreateChild<Ui::PopupMenu>(parent);
  284. menu->addAction(copyButton, [=] {
  285. QGuiApplication::clipboard()->setText(text);
  286. });
  287. menu->popup(QCursor::pos());
  288. }
  289. void AddRow(
  290. not_null<Ui::VerticalLayout*> container,
  291. rpl::producer<QString> label,
  292. rpl::producer<TextWithEntities> value,
  293. const QString &copyButton,
  294. Fn<void()> edit,
  295. IconDescriptor &&descriptor) {
  296. const auto wrap = AddButtonWithLabel(
  297. container,
  298. std::move(label),
  299. std::move(value) | rpl::map([](const auto &t) { return t.text; }),
  300. st::settingsButton,
  301. std::move(descriptor));
  302. const auto forcopy = Ui::CreateChild<QString>(wrap.get());
  303. wrap->setAcceptBoth();
  304. wrap->clicks(
  305. ) | rpl::filter([=] {
  306. return !wrap->isDisabled();
  307. }) | rpl::start_with_next([=](Qt::MouseButton button) {
  308. if (button == Qt::LeftButton) {
  309. edit();
  310. } else if (!forcopy->isEmpty()) {
  311. ShowMenu(wrap, copyButton, *forcopy);
  312. }
  313. }, wrap->lifetime());
  314. auto existing = base::duplicate(
  315. value
  316. ) | rpl::map([](const TextWithEntities &text) {
  317. return text.entities.isEmpty();
  318. });
  319. base::duplicate(
  320. value
  321. ) | rpl::filter([](const TextWithEntities &text) {
  322. return text.entities.isEmpty();
  323. }) | rpl::start_with_next([=](const TextWithEntities &text) {
  324. *forcopy = text.text;
  325. }, wrap->lifetime());
  326. }
  327. void SetupBirthday(
  328. not_null<Ui::VerticalLayout*> container,
  329. not_null<Window::SessionController*> controller,
  330. not_null<UserData*> self) {
  331. const auto session = &self->session();
  332. Ui::AddSkip(container);
  333. auto value = rpl::combine(
  334. Info::Profile::BirthdayValue(self),
  335. tr::lng_settings_birthday_add()
  336. ) | rpl::map([](Data::Birthday birthday, const QString &add) {
  337. const auto text = Data::BirthdayText(birthday);
  338. return TextWithEntities{ !text.isEmpty() ? text : add };
  339. });
  340. const auto edit = [=] {
  341. Core::App().openInternalUrl(
  342. u"internal:edit_birthday"_q,
  343. QVariant::fromValue(ClickHandlerContext{
  344. .sessionWindow = base::make_weak(controller),
  345. }));
  346. };
  347. AddRow(
  348. container,
  349. tr::lng_settings_birthday_label(),
  350. std::move(value),
  351. tr::lng_mediaview_copy(tr::now),
  352. edit,
  353. { &st::menuIconGiftPremium });
  354. const auto key = Api::UserPrivacy::Key::Birthday;
  355. session->api().userPrivacy().reload(key);
  356. auto isExactlyContacts = session->api().userPrivacy().value(
  357. key
  358. ) | rpl::map([=](const Api::UserPrivacy::Rule &value) {
  359. return (value.option == Api::UserPrivacy::Option::Contacts)
  360. && value.always.peers.empty()
  361. && !value.always.premiums
  362. && value.never.peers.empty();
  363. }) | rpl::distinct_until_changed();
  364. Ui::AddSkip(container);
  365. Ui::AddDividerText(container, rpl::conditional(
  366. std::move(isExactlyContacts),
  367. tr::lng_settings_birthday_contacts(
  368. lt_link,
  369. tr::lng_settings_birthday_contacts_link(
  370. ) | Ui::Text::ToLink(u"internal:edit_privacy_birthday"_q),
  371. Ui::Text::WithEntities),
  372. tr::lng_settings_birthday_about(
  373. lt_link,
  374. tr::lng_settings_birthday_about_link(
  375. ) | Ui::Text::ToLink(u"internal:edit_privacy_birthday"_q),
  376. Ui::Text::WithEntities)));
  377. }
  378. void SetupPersonalChannel(
  379. not_null<Ui::VerticalLayout*> container,
  380. not_null<Window::SessionController*> controller,
  381. not_null<UserData*> self) {
  382. Ui::AddSkip(container);
  383. auto value = rpl::combine(
  384. Info::Profile::PersonalChannelValue(self),
  385. tr::lng_settings_channel_add()
  386. ) | rpl::map([](ChannelData *channel, const QString &add) {
  387. return TextWithEntities{ channel ? channel->name() : add };
  388. });
  389. const auto edit = [=] {
  390. Core::App().openInternalUrl(
  391. u"internal:edit_personal_channel"_q,
  392. QVariant::fromValue(ClickHandlerContext{
  393. .sessionWindow = base::make_weak(controller),
  394. }));
  395. };
  396. AddRow(
  397. container,
  398. tr::lng_settings_channel_label(),
  399. std::move(value),
  400. tr::lng_mediaview_copy(tr::now),
  401. edit,
  402. { &st::menuIconChannel });
  403. Ui::AddSkip(container);
  404. Ui::AddDivider(container);
  405. }
  406. void SetupRows(
  407. not_null<Ui::VerticalLayout*> container,
  408. not_null<Window::SessionController*> controller,
  409. not_null<UserData*> self) {
  410. const auto session = &self->session();
  411. Ui::AddSkip(container);
  412. AddRow(
  413. container,
  414. tr::lng_settings_name_label(),
  415. Info::Profile::NameValue(self) | Ui::Text::ToWithEntities(),
  416. tr::lng_profile_copy_fullname(tr::now),
  417. [=] { controller->show(Box<EditNameBox>(self)); },
  418. { &st::menuIconProfile });
  419. const auto showChangePhone = [=] {
  420. controller->show(
  421. Ui::MakeInformBox(tr::lng_change_phone_error()));
  422. controller->window().activate();
  423. };
  424. AddRow(
  425. container,
  426. tr::lng_settings_phone_label(),
  427. Info::Profile::PhoneValue(self),
  428. tr::lng_profile_copy_phone(tr::now),
  429. showChangePhone,
  430. { &st::menuIconPhone });
  431. auto username = Info::Profile::UsernameValue(self);
  432. auto empty = base::duplicate(
  433. username
  434. ) | rpl::map([](const TextWithEntities &username) {
  435. return username.text.isEmpty();
  436. });
  437. auto label = rpl::combine(
  438. tr::lng_settings_username_label(),
  439. std::move(empty)
  440. ) | rpl::map([](const QString &label, bool empty) {
  441. return empty ? "t.me/username" : label;
  442. });
  443. auto value = rpl::combine(
  444. std::move(username),
  445. tr::lng_settings_username_add()
  446. ) | rpl::map([](const TextWithEntities &username, const QString &add) {
  447. if (!username.text.isEmpty()) {
  448. return username;
  449. }
  450. auto result = TextWithEntities{ add };
  451. result.entities.push_back({
  452. EntityType::CustomUrl,
  453. 0,
  454. int(add.size()),
  455. "internal:edit_username" });
  456. return result;
  457. });
  458. session->api().usernames().requestToCache(session->user());
  459. AddRow(
  460. container,
  461. std::move(label),
  462. std::move(value),
  463. tr::lng_context_copy_mention(tr::now),
  464. [=] {
  465. const auto box = controller->show(
  466. Box(UsernamesBox, session->user()));
  467. box->boxClosing(
  468. ) | rpl::start_with_next([=] {
  469. session->api().usernames().requestToCache(session->user());
  470. }, box->lifetime());
  471. },
  472. { &st::menuIconUsername });
  473. Ui::AddSkip(container);
  474. Ui::AddDividerText(container, tr::lng_settings_username_about());
  475. }
  476. void SetupBio(
  477. not_null<Ui::VerticalLayout*> container,
  478. not_null<UserData*> self) {
  479. const auto limits = Data::PremiumLimits(&self->session());
  480. const auto defaultLimit = limits.aboutLengthDefault();
  481. const auto premiumLimit = limits.aboutLengthPremium();
  482. const auto bioStyle = [=] {
  483. auto result = st::settingsBio;
  484. result.textMargins.setRight(st::boxTextFont->spacew
  485. + st::boxTextFont->width('-' + QString::number(premiumLimit)));
  486. return result;
  487. };
  488. const auto style = Ui::AttachAsChild(container, bioStyle());
  489. const auto current = Ui::AttachAsChild(container, self->about());
  490. const auto changed = Ui::CreateChild<rpl::event_stream<bool>>(
  491. container.get());
  492. const auto bio = container->add(
  493. object_ptr<Ui::InputField>(
  494. container,
  495. *style,
  496. Ui::InputField::Mode::MultiLine,
  497. tr::lng_bio_placeholder(),
  498. *current),
  499. st::settingsBioMargins);
  500. const auto countdown = Ui::CreateChild<Ui::FlatLabel>(
  501. container.get(),
  502. QString(),
  503. st::settingsBioCountdown);
  504. rpl::combine(
  505. bio->geometryValue(),
  506. countdown->widthValue()
  507. ) | rpl::start_with_next([=](QRect geometry, int width) {
  508. countdown->move(
  509. geometry.x() + geometry.width() - width,
  510. geometry.y() + style->textMargins.top());
  511. }, countdown->lifetime());
  512. const auto assign = [=](QString text) {
  513. auto position = bio->textCursor().position();
  514. bio->setText(text.replace('\n', ' '));
  515. auto cursor = bio->textCursor();
  516. cursor.setPosition(position);
  517. bio->setTextCursor(cursor);
  518. };
  519. const auto updated = [=] {
  520. auto text = bio->getLastText();
  521. if (text.indexOf('\n') >= 0) {
  522. assign(text);
  523. text = bio->getLastText();
  524. }
  525. changed->fire(*current != text);
  526. const auto limit = self->isPremium() ? premiumLimit : defaultLimit;
  527. const auto countLeft = limit - Ui::ComputeFieldCharacterCount(bio);
  528. countdown->setText(QString::number(countLeft));
  529. countdown->setTextColorOverride(
  530. countLeft < 0 ? st::boxTextFgError->c : std::optional<QColor>());
  531. };
  532. const auto save = [=] {
  533. self->session().api().saveSelfBio(
  534. TextUtilities::PrepareForSending(bio->getLastText()));
  535. };
  536. Info::Profile::AboutValue(
  537. self
  538. ) | rpl::start_with_next([=](const TextWithEntities &text) {
  539. const auto wasChanged = (*current != bio->getLastText());
  540. *current = text.text;
  541. if (wasChanged) {
  542. changed->fire(*current != bio->getLastText());
  543. } else {
  544. assign(text.text);
  545. *current = bio->getLastText();
  546. }
  547. }, bio->lifetime());
  548. const auto generation = Ui::CreateChild<int>(bio);
  549. changed->events(
  550. ) | rpl::start_with_next([=](bool changed) {
  551. if (changed) {
  552. const auto saved = *generation = std::abs(*generation) + 1;
  553. base::call_delayed(kSaveBioTimeout, bio, [=] {
  554. if (*generation == saved) {
  555. save();
  556. *generation = 0;
  557. }
  558. });
  559. } else if (*generation > 0) {
  560. *generation = -*generation;
  561. }
  562. }, bio->lifetime());
  563. // We need 'bio' to still exist here as InputField, so we add this
  564. // to 'container' lifetime, not to the 'bio' lifetime.
  565. container->lifetime().add([=] {
  566. if (*generation > 0) {
  567. save();
  568. }
  569. });
  570. bio->setMaxLength(premiumLimit * 2);
  571. bio->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
  572. auto cursor = bio->textCursor();
  573. cursor.setPosition(bio->getLastText().size());
  574. bio->setTextCursor(cursor);
  575. bio->submits() | rpl::start_with_next([=] { save(); }, bio->lifetime());
  576. bio->changes() | rpl::start_with_next(updated, bio->lifetime());
  577. bio->setInstantReplaces(Ui::InstantReplaces::Default());
  578. bio->setInstantReplacesEnabled(
  579. Core::App().settings().replaceEmojiValue());
  580. Ui::Emoji::SuggestionsController::Init(
  581. container->window(),
  582. bio,
  583. &self->session());
  584. updated();
  585. Ui::AddDividerText(container, tr::lng_settings_about_bio());
  586. }
  587. void SetupAccountsWrap(
  588. not_null<Ui::VerticalLayout*> container,
  589. not_null<Window::SessionController*> controller) {
  590. Ui::AddSkip(container);
  591. SetupAccounts(container, controller);
  592. }
  593. [[nodiscard]] bool IsAltShift(Qt::KeyboardModifiers modifiers) {
  594. return (modifiers & Qt::ShiftModifier) && (modifiers & Qt::AltModifier);
  595. }
  596. [[nodiscard]] object_ptr<Ui::SettingsButton> MakeAccountButton(
  597. QWidget *parent,
  598. not_null<Window::SessionController*> window,
  599. not_null<Main::Account*> account,
  600. Fn<void(Qt::KeyboardModifiers)> callback,
  601. bool locked) {
  602. const auto active = (account == &window->session().account());
  603. const auto session = &account->session();
  604. const auto user = session->user();
  605. auto text = rpl::single(
  606. user->name()
  607. ) | rpl::then(session->changes().realtimeNameUpdates(
  608. user
  609. ) | rpl::map([=] {
  610. return user->name();
  611. }));
  612. auto result = object_ptr<Ui::SettingsButton>(
  613. parent,
  614. rpl::duplicate(text),
  615. st::mainMenuAddAccountButton);
  616. const auto raw = result.data();
  617. {
  618. const auto container = Badge::AddRight(raw);
  619. const auto composedBadge = Ui::CreateChild<ComposedBadge>(
  620. container.get(),
  621. raw,
  622. session,
  623. std::move(text),
  624. !active,
  625. [=] { return window->isGifPausedAtLeastFor(
  626. Window::GifPauseReason::Layer); });
  627. composedBadge->sizeValue(
  628. ) | rpl::start_with_next([=](const QSize &s) {
  629. container->resize(s);
  630. }, container->lifetime());
  631. }
  632. struct State {
  633. State(QWidget *parent) : userpic(parent) {
  634. userpic.setAttribute(Qt::WA_TransparentForMouseEvents);
  635. }
  636. Ui::RpWidget userpic;
  637. Ui::PeerUserpicView view;
  638. base::unique_qptr<Ui::PopupMenu> menu;
  639. };
  640. const auto state = raw->lifetime().make_state<State>(raw);
  641. const auto userpicSkip = 2 * st::mainMenuAccountLine + st::lineWidth;
  642. const auto userpicSize = st::mainMenuAccountSize
  643. + userpicSkip * 2;
  644. raw->heightValue(
  645. ) | rpl::start_with_next([=](int height) {
  646. const auto left = st::mainMenuAddAccountButton.iconLeft
  647. + (st::settingsIconAdd.width() - userpicSize) / 2;
  648. const auto top = (height - userpicSize) / 2;
  649. state->userpic.setGeometry(left, top, userpicSize, userpicSize);
  650. }, state->userpic.lifetime());
  651. state->userpic.paintRequest(
  652. ) | rpl::start_with_next([=] {
  653. auto p = Painter(&state->userpic);
  654. const auto size = st::mainMenuAccountSize;
  655. const auto line = st::mainMenuAccountLine;
  656. const auto skip = 2 * line + st::lineWidth;
  657. const auto full = size + skip * 2;
  658. user->paintUserpicLeft(p, state->view, skip, skip, full, size);
  659. if (active) {
  660. const auto shift = st::lineWidth + (line * 0.5);
  661. const auto diameter = full - 2 * shift;
  662. const auto rect = QRectF(shift, shift, diameter, diameter);
  663. auto hq = PainterHighQualityEnabler(p);
  664. auto pen = st::windowBgActive->p; // The same as '+' in add.
  665. pen.setWidthF(line);
  666. p.setPen(pen);
  667. p.setBrush(Qt::NoBrush);
  668. p.drawEllipse(rect);
  669. }
  670. }, state->userpic.lifetime());
  671. raw->setAcceptBoth(true);
  672. raw->clicks(
  673. ) | rpl::start_with_next([=](Qt::MouseButton which) {
  674. if (which == Qt::LeftButton) {
  675. callback(raw->clickModifiers());
  676. return;
  677. } else if (which == Qt::MiddleButton) {
  678. callback(Qt::ControlModifier);
  679. return;
  680. } else if (which != Qt::RightButton) {
  681. return;
  682. }
  683. if (state->menu) {
  684. return;
  685. }
  686. const auto isActive = session == &window->session();
  687. state->menu = base::make_unique_q<Ui::PopupMenu>(
  688. raw,
  689. st::popupMenuExpandedSeparator);
  690. const auto addAction = Ui::Menu::CreateAddActionCallback(
  691. state->menu);
  692. if (!isActive) {
  693. addAction(tr::lng_context_new_window(tr::now), [=] {
  694. Ui::PreventDelayedActivation();
  695. callback(Qt::ControlModifier);
  696. }, &st::menuIconNewWindow);
  697. Window::AddSeparatorAndShiftUp(addAction);
  698. }
  699. addAction(tr::lng_profile_copy_phone(tr::now), [=] {
  700. const auto phone = rpl::variable<TextWithEntities>(
  701. Info::Profile::PhoneValue(session->user()));
  702. QGuiApplication::clipboard()->setText(phone.current().text);
  703. }, &st::menuIconCopy);
  704. if (!locked) {
  705. if (!isActive) {
  706. addAction(tr::lng_menu_activate(tr::now), [=] {
  707. callback({});
  708. }, &st::menuIconProfile);
  709. }
  710. Window::MenuAddMarkAsReadAllChatsAction(
  711. session,
  712. window->uiShow(),
  713. addAction);
  714. }
  715. if (!isActive) {
  716. auto logoutCallback = [=] {
  717. const auto callback = [=](Fn<void()> &&close) {
  718. close();
  719. Core::App().logoutWithChecks(&session->account());
  720. };
  721. window->show(
  722. Ui::MakeConfirmBox({
  723. .text = tr::lng_sure_logout(),
  724. .confirmed = crl::guard(session, callback),
  725. .confirmText = tr::lng_settings_logout(),
  726. .confirmStyle = &st::attentionBoxButton,
  727. }),
  728. Ui::LayerOption::CloseOther);
  729. };
  730. addAction({
  731. .text = tr::lng_settings_logout(tr::now),
  732. .handler = std::move(logoutCallback),
  733. .icon = &st::menuIconLeaveAttention,
  734. .isAttention = true,
  735. });
  736. }
  737. state->menu->popup(QCursor::pos());
  738. }, raw->lifetime());
  739. return result;
  740. }
  741. AccountsList::AccountsList(
  742. not_null<Ui::VerticalLayout*> container,
  743. not_null<Window::SessionController*> controller)
  744. : _controller(controller)
  745. , _outer(container)
  746. , _outerIndex(container->count()) {
  747. setup();
  748. }
  749. rpl::producer<> AccountsList::closeRequests() const {
  750. return _closeRequests.events();
  751. }
  752. void AccountsList::setup() {
  753. _addAccount = setupAdd();
  754. rpl::single(rpl::empty) | rpl::then(
  755. Core::App().domain().accountsChanges()
  756. ) | rpl::start_with_next([=] {
  757. const auto &list = Core::App().domain().accounts();
  758. const auto exists = [&](not_null<Main::Account*> account) {
  759. for (const auto &[index, existing] : list) {
  760. if (account == existing.get()) {
  761. return true;
  762. }
  763. }
  764. return false;
  765. };
  766. for (auto i = _watched.begin(); i != _watched.end();) {
  767. if (!exists(i->first)) {
  768. i = _watched.erase(i);
  769. } else {
  770. ++i;
  771. }
  772. }
  773. for (const auto &[index, account] : list) {
  774. if (_watched.emplace(account.get()).second) {
  775. account->sessionChanges(
  776. ) | rpl::start_with_next([=] {
  777. rebuild();
  778. }, _outer->lifetime());
  779. }
  780. }
  781. rebuild();
  782. }, _outer->lifetime());
  783. Core::App().domain().maxAccountsChanges(
  784. ) | rpl::start_with_next([=] {
  785. // Full rebuild.
  786. for (auto i = _watched.begin(); i != _watched.end(); i++) {
  787. i->second = nullptr;
  788. }
  789. rebuild();
  790. }, _outer->lifetime());
  791. }
  792. not_null<Ui::SlideWrap<Ui::SettingsButton>*> AccountsList::setupAdd() {
  793. const auto result = _outer->add(
  794. object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  795. _outer.get(),
  796. CreateButtonWithIcon(
  797. _outer.get(),
  798. tr::lng_menu_add_account(),
  799. st::mainMenuAddAccountButton,
  800. {
  801. &st::settingsIconAdd,
  802. IconType::Round,
  803. &st::windowBgActive
  804. })))->setDuration(0);
  805. const auto button = result->entity();
  806. using Environment = MTP::Environment;
  807. const auto add = [=](Environment environment, bool newWindow = false) {
  808. auto &domain = _controller->session().domain();
  809. auto found = false;
  810. for (const auto &[index, account] : domain.accounts()) {
  811. const auto raw = account.get();
  812. if (!raw->sessionExists()
  813. && raw->mtp().environment() == environment) {
  814. found = true;
  815. }
  816. }
  817. if (!found && domain.accounts().size() >= domain.maxAccounts()) {
  818. _controller->show(
  819. Box(AccountsLimitBox, &_controller->session()));
  820. } else if (newWindow) {
  821. domain.addActivated(environment, true);
  822. } else {
  823. _controller->window().preventOrInvoke([=] {
  824. _controller->session().domain().addActivated(environment);
  825. });
  826. }
  827. };
  828. button->setAcceptBoth(true);
  829. button->clicks(
  830. ) | rpl::start_with_next([=](Qt::MouseButton which) {
  831. if (which == Qt::LeftButton) {
  832. const auto modifiers = button->clickModifiers();
  833. const auto newWindow = (modifiers & Qt::ControlModifier);
  834. add(Environment::Production, newWindow);
  835. return;
  836. } else if (which != Qt::RightButton
  837. || !IsAltShift(button->clickModifiers())) {
  838. return;
  839. }
  840. _contextMenu = base::make_unique_q<Ui::PopupMenu>(_outer);
  841. _contextMenu->addAction("Production Server", [=] {
  842. add(Environment::Production);
  843. });
  844. _contextMenu->addAction("Test Server", [=] {
  845. add(Environment::Test);
  846. });
  847. _contextMenu->popup(QCursor::pos());
  848. }, button->lifetime());
  849. return result;
  850. }
  851. void AccountsList::rebuild() {
  852. const auto inner = _outer->insert(
  853. _outerIndex,
  854. object_ptr<Ui::VerticalLayout>(_outer.get()));
  855. _reorder = std::make_unique<Ui::VerticalLayoutReorder>(inner);
  856. _reorder->updates(
  857. ) | rpl::start_with_next([=](Ui::VerticalLayoutReorder::Single data) {
  858. using State = Ui::VerticalLayoutReorder::State;
  859. if (data.state == State::Started) {
  860. ++_reordering;
  861. } else {
  862. Ui::PostponeCall(inner, [=] {
  863. --_reordering;
  864. });
  865. if (data.state == State::Applied) {
  866. std::vector<uint64> order;
  867. order.reserve(inner->count());
  868. for (auto i = 0; i < inner->count(); i++) {
  869. for (const auto &[account, button] : _watched) {
  870. if (button.get() == inner->widgetAt(i)) {
  871. order.push_back(account->session().uniqueId());
  872. }
  873. }
  874. }
  875. Core::App().settings().setAccountsOrder(order);
  876. Core::App().saveSettings();
  877. }
  878. }
  879. }, inner->lifetime());
  880. const auto premiumLimit = _controller->session().domain().maxAccounts();
  881. const auto list = _controller->session().domain().orderedAccounts();
  882. for (const auto &account : list) {
  883. auto i = _watched.find(account);
  884. Assert(i != _watched.end());
  885. auto &button = i->second;
  886. if (!account->sessionExists() || list.size() == 1) {
  887. button = nullptr;
  888. } else if (!button) {
  889. const auto nextIsLocked = (inner->count() >= premiumLimit);
  890. auto callback = [=](Qt::KeyboardModifiers modifiers) {
  891. if (_reordering) {
  892. return;
  893. }
  894. if (account == &_controller->session().account()) {
  895. _closeRequests.fire({});
  896. return;
  897. }
  898. const auto newWindow = (modifiers & Qt::ControlModifier);
  899. auto activate = [=, guard = _accountSwitchGuard.make_guard()]{
  900. if (guard) {
  901. _reorder->finishReordering();
  902. if (newWindow) {
  903. _closeRequests.fire({});
  904. Core::App().ensureSeparateWindowFor(account);
  905. }
  906. Core::App().domain().maybeActivate(account);
  907. }
  908. };
  909. if (const auto window = Core::App().separateWindowFor(
  910. account)) {
  911. _closeRequests.fire({});
  912. window->activate();
  913. } else {
  914. base::call_delayed(
  915. st::defaultRippleAnimation.hideDuration,
  916. account,
  917. std::move(activate));
  918. }
  919. };
  920. button.reset(inner->add(MakeAccountButton(
  921. inner,
  922. _controller,
  923. account,
  924. std::move(callback),
  925. nextIsLocked)));
  926. }
  927. }
  928. inner->resizeToWidth(_outer->width());
  929. const auto count = int(list.size());
  930. _reorder->addPinnedInterval(
  931. premiumLimit,
  932. std::max(1, count - premiumLimit));
  933. _addAccount->toggle(
  934. (count < Main::Domain::kPremiumMaxAccounts),
  935. anim::type::instant);
  936. _reorder->start();
  937. }
  938. } // namespace
  939. Information::Information(
  940. QWidget *parent,
  941. not_null<Window::SessionController*> controller)
  942. : Section(parent) {
  943. setupContent(controller);
  944. }
  945. rpl::producer<QString> Information::title() {
  946. return tr::lng_settings_section_info();
  947. }
  948. void Information::setupContent(
  949. not_null<Window::SessionController*> controller) {
  950. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  951. const auto self = controller->session().user();
  952. SetupPhoto(content, controller, self);
  953. SetupBio(content, self);
  954. SetupRows(content, controller, self);
  955. SetupPersonalChannel(content, controller, self);
  956. SetupBirthday(content, controller, self);
  957. SetupAccountsWrap(content, controller);
  958. Ui::ResizeFitChild(this, content);
  959. }
  960. AccountsEvents SetupAccounts(
  961. not_null<Ui::VerticalLayout*> container,
  962. not_null<Window::SessionController*> controller) {
  963. const auto list = container->lifetime().make_state<AccountsList>(
  964. container,
  965. controller);
  966. return {
  967. .closeRequests = list->closeRequests(),
  968. };
  969. }
  970. void UpdatePhotoLocally(not_null<UserData*> user, const QImage &image) {
  971. auto bytes = QByteArray();
  972. auto buffer = QBuffer(&bytes);
  973. image.save(&buffer, "JPG", 87);
  974. user->setUserpic(
  975. base::RandomValue<PhotoId>(),
  976. ImageLocation(
  977. { .data = InMemoryLocation{ .bytes = bytes } },
  978. image.width(),
  979. image.height()),
  980. false);
  981. }
  982. namespace Badge {
  983. Ui::UnreadBadgeStyle Style() {
  984. auto result = Ui::UnreadBadgeStyle();
  985. result.font = st::mainMenuBadgeFont;
  986. result.size = st::mainMenuBadgeSize;
  987. result.sizeId = Ui::UnreadBadgeSize::MainMenu;
  988. return result;
  989. }
  990. not_null<Ui::RpWidget*> AddRight(
  991. not_null<Ui::SettingsButton*> button) {
  992. const auto widget = Ui::CreateChild<Ui::RpWidget>(button.get());
  993. rpl::combine(
  994. button->sizeValue(),
  995. widget->sizeValue(),
  996. widget->shownValue()
  997. ) | rpl::start_with_next([=](QSize outer, QSize inner, bool shown) {
  998. auto padding = button->st().padding;
  999. if (shown) {
  1000. widget->moveToRight(
  1001. padding.right(),
  1002. (outer.height() - inner.height()) / 2,
  1003. outer.width());
  1004. padding.setRight(padding.right() + inner.width());
  1005. }
  1006. button->setPaddingOverride(padding);
  1007. button->update();
  1008. }, widget->lifetime());
  1009. return widget;
  1010. }
  1011. not_null<Ui::RpWidget*> CreateUnread(
  1012. not_null<Ui::RpWidget*> container,
  1013. rpl::producer<UnreadBadge> value) {
  1014. struct State {
  1015. State(QWidget *parent) : widget(parent) {
  1016. widget.setAttribute(Qt::WA_TransparentForMouseEvents);
  1017. }
  1018. Ui::RpWidget widget;
  1019. Ui::UnreadBadgeStyle st = Style();
  1020. int count = 0;
  1021. QString string;
  1022. };
  1023. const auto state = container->lifetime().make_state<State>(container);
  1024. std::move(
  1025. value
  1026. ) | rpl::start_with_next([=](UnreadBadge badge) {
  1027. state->st.muted = badge.muted;
  1028. state->count = badge.count;
  1029. if (!state->count) {
  1030. state->widget.hide();
  1031. return;
  1032. }
  1033. state->string = Lang::FormatCountToShort(state->count).string;
  1034. state->widget.resize(Ui::CountUnreadBadgeSize(state->string, state->st));
  1035. if (state->widget.isHidden()) {
  1036. state->widget.show();
  1037. }
  1038. }, state->widget.lifetime());
  1039. state->widget.paintRequest(
  1040. ) | rpl::start_with_next([=] {
  1041. auto p = Painter(&state->widget);
  1042. Ui::PaintUnreadBadge(
  1043. p,
  1044. state->string,
  1045. state->widget.width(),
  1046. 0,
  1047. state->st);
  1048. }, state->widget.lifetime());
  1049. return &state->widget;
  1050. }
  1051. void AddUnread(
  1052. not_null<Ui::SettingsButton*> button,
  1053. rpl::producer<UnreadBadge> value) {
  1054. const auto container = AddRight(button);
  1055. const auto badge = CreateUnread(container, std::move(value));
  1056. badge->sizeValue(
  1057. ) | rpl::start_with_next([=](const QSize &s) {
  1058. container->resize(s);
  1059. }, container->lifetime());
  1060. }
  1061. } // namespace Badge
  1062. } // namespace Settings