edit_privacy_box.cpp 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  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 "boxes/edit_privacy_box.h"
  8. #include "api/api_global_privacy.h"
  9. #include "boxes/filters/edit_filter_chats_list.h"
  10. #include "ui/effects/premium_graphics.h"
  11. #include "ui/layers/generic_box.h"
  12. #include "ui/widgets/checkbox.h"
  13. #include "ui/widgets/continuous_sliders.h"
  14. #include "ui/widgets/shadow.h"
  15. #include "ui/text/format_values.h"
  16. #include "ui/text/text_utilities.h"
  17. #include "ui/toast/toast.h"
  18. #include "ui/wrap/slide_wrap.h"
  19. #include "ui/painter.h"
  20. #include "ui/vertical_list.h"
  21. #include "history/history.h"
  22. #include "boxes/peer_list_controllers.h"
  23. #include "settings/settings_premium.h"
  24. #include "settings/settings_privacy_controllers.h"
  25. #include "settings/settings_privacy_security.h"
  26. #include "calls/calls_instance.h"
  27. #include "lang/lang_keys.h"
  28. #include "apiwrap.h"
  29. #include "main/main_app_config.h"
  30. #include "main/main_session.h"
  31. #include "data/data_user.h"
  32. #include "data/data_chat.h"
  33. #include "data/data_channel.h"
  34. #include "data/data_peer_values.h"
  35. #include "window/window_session_controller.h"
  36. #include "styles/style_boxes.h"
  37. #include "styles/style_settings.h"
  38. #include "styles/style_layers.h"
  39. #include "styles/style_menu_icons.h"
  40. #include "styles/style_window.h"
  41. namespace {
  42. constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;
  43. constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;
  44. constexpr auto kStarsMin = 1;
  45. constexpr auto kDefaultChargeStars = 10;
  46. using Exceptions = Api::UserPrivacy::Exceptions;
  47. enum class SpecialRowType {
  48. Premiums,
  49. MiniApps,
  50. };
  51. [[nodiscard]] PaintRoundImageCallback GeneratePremiumsUserpicCallback(
  52. bool forceRound) {
  53. return [=](QPainter &p, int x, int y, int outerWidth, int size) {
  54. auto gradient = QLinearGradient(
  55. QPointF(x, y),
  56. QPointF(x + size, y + size));
  57. gradient.setStops(Ui::Premium::ButtonGradientStops());
  58. auto hq = PainterHighQualityEnabler(p);
  59. p.setPen(Qt::NoPen);
  60. p.setBrush(gradient);
  61. if (forceRound) {
  62. p.drawEllipse(x, y, size, size);
  63. } else {
  64. const auto radius = size * Ui::ForumUserpicRadiusMultiplier();
  65. p.drawRoundedRect(x, y, size, size, radius, radius);
  66. }
  67. st::settingsPrivacyPremium.paintInCenter(p, QRect(x, y, size, size));
  68. };
  69. }
  70. [[nodiscard]] PaintRoundImageCallback GenerateMiniAppsUserpicCallback(
  71. bool forceRound) {
  72. return [=](QPainter &p, int x, int y, int outerWidth, int size) {
  73. const auto &color1 = st::historyPeer6UserpicBg;
  74. const auto &color2 = st::historyPeer6UserpicBg2;
  75. auto hq = PainterHighQualityEnabler(p);
  76. auto gradient = QLinearGradient(x, y, x, y + size);
  77. gradient.setStops({ { 0., color1->c }, { 1., color2->c } });
  78. p.setPen(Qt::NoPen);
  79. p.setBrush(gradient);
  80. if (forceRound) {
  81. p.drawEllipse(x, y, size, size);
  82. } else {
  83. const auto radius = size * Ui::ForumUserpicRadiusMultiplier();
  84. p.drawRoundedRect(x, y, size, size, radius, radius);
  85. }
  86. st::windowFilterTypeBots.paintInCenter(p, QRect(x, y, size, size));
  87. };
  88. }
  89. void CreateRadiobuttonLock(
  90. not_null<Ui::RpWidget*> widget,
  91. const style::Checkbox &st) {
  92. const auto lock = Ui::CreateChild<Ui::RpWidget>(widget.get());
  93. lock->setAttribute(Qt::WA_TransparentForMouseEvents);
  94. lock->resize(st::defaultRadio.diameter, st::defaultRadio.diameter);
  95. widget->sizeValue(
  96. ) | rpl::start_with_next([=, &st](QSize size) {
  97. lock->move(st.checkPosition);
  98. }, lock->lifetime());
  99. lock->paintRequest() | rpl::start_with_next([=] {
  100. auto p = QPainter(lock);
  101. auto hq = PainterHighQualityEnabler(p);
  102. const auto &icon = st::messagePrivacyLock;
  103. const auto size = st::defaultRadio.diameter;
  104. const auto image = icon.instance(st::checkboxFg->c);
  105. p.drawImage(QRectF(
  106. (size - icon.width()) / 2.,
  107. (size - icon.height()) / 2.,
  108. icon.width(),
  109. icon.height()), image);
  110. }, lock->lifetime());
  111. }
  112. void AddPremiumRequiredRow(
  113. not_null<Ui::RpWidget*> widget,
  114. not_null<Main::Session*> session,
  115. Fn<void()> clickedCallback,
  116. Fn<void()> setDefaultOption,
  117. const style::Checkbox &st) {
  118. const auto row = Ui::CreateChild<Ui::AbstractButton>(widget.get());
  119. widget->sizeValue(
  120. ) | rpl::start_with_next([=](const QSize &s) {
  121. row->resize(s);
  122. }, row->lifetime());
  123. row->setClickedCallback(std::move(clickedCallback));
  124. CreateRadiobuttonLock(row, st);
  125. Data::AmPremiumValue(
  126. session
  127. ) | rpl::start_with_next([=](bool premium) {
  128. row->setVisible(!premium);
  129. if (!premium) {
  130. setDefaultOption();
  131. }
  132. }, row->lifetime());
  133. }
  134. class PrivacyExceptionsBoxController : public ChatsListBoxController {
  135. public:
  136. PrivacyExceptionsBoxController(
  137. not_null<Main::Session*> session,
  138. rpl::producer<QString> title,
  139. const Exceptions &selected,
  140. std::optional<SpecialRowType> allowChooseSpecial);
  141. Main::Session &session() const override;
  142. void rowClicked(not_null<PeerListRow*> row) override;
  143. bool isForeignRow(PeerListRowId itemId) override;
  144. bool handleDeselectForeignRow(PeerListRowId itemId) override;
  145. [[nodiscard]] bool premiumsSelected() const;
  146. [[nodiscard]] bool miniAppsSelected() const;
  147. protected:
  148. void prepareViewHook() override;
  149. std::unique_ptr<Row> createRow(not_null<History*> history) override;
  150. private:
  151. [[nodiscard]] object_ptr<Ui::RpWidget> prepareSpecialRowList(
  152. SpecialRowType type);
  153. const not_null<Main::Session*> _session;
  154. rpl::producer<QString> _title;
  155. Exceptions _selected;
  156. std::optional<SpecialRowType> _allowChooseSpecial;
  157. PeerListContentDelegate *_typesDelegate = nullptr;
  158. Fn<void(PeerListRowId)> _deselectOption;
  159. };
  160. struct RowSelectionChange {
  161. not_null<PeerListRow*> row;
  162. bool checked = false;
  163. };
  164. class SpecialRow final : public PeerListRow {
  165. public:
  166. explicit SpecialRow(SpecialRowType type);
  167. QString generateName() override;
  168. QString generateShortName() override;
  169. PaintRoundImageCallback generatePaintUserpicCallback(
  170. bool forceRound) override;
  171. bool useForumLikeUserpic() const override;
  172. };
  173. class TypesController final : public PeerListController {
  174. public:
  175. TypesController(not_null<Main::Session*> session, SpecialRowType type);
  176. Main::Session &session() const override;
  177. void prepare() override;
  178. void rowClicked(not_null<PeerListRow*> row) override;
  179. [[nodiscard]] bool specialSelected() const;
  180. [[nodiscard]] rpl::producer<bool> specialChanges() const;
  181. [[nodiscard]] auto rowSelectionChanges() const
  182. -> rpl::producer<RowSelectionChange>;
  183. private:
  184. const not_null<Main::Session*> _session;
  185. const SpecialRowType _type;
  186. rpl::event_stream<> _selectionChanged;
  187. rpl::event_stream<RowSelectionChange> _rowSelectionChanges;
  188. };
  189. SpecialRow::SpecialRow(SpecialRowType type)
  190. : PeerListRow((type == SpecialRowType::Premiums)
  191. ? kPremiumsRowId
  192. : kMiniAppsRowId) {
  193. setCustomStatus((id() == kPremiumsRowId)
  194. ? tr::lng_edit_privacy_premium_status(tr::now)
  195. : tr::lng_edit_privacy_miniapps_status(tr::now));
  196. }
  197. QString SpecialRow::generateName() {
  198. return (id() == kPremiumsRowId)
  199. ? tr::lng_edit_privacy_premium(tr::now)
  200. : tr::lng_edit_privacy_miniapps(tr::now);
  201. }
  202. QString SpecialRow::generateShortName() {
  203. return generateName();
  204. }
  205. PaintRoundImageCallback SpecialRow::generatePaintUserpicCallback(
  206. bool forceRound) {
  207. return (id() == kPremiumsRowId)
  208. ? GeneratePremiumsUserpicCallback(forceRound)
  209. : GenerateMiniAppsUserpicCallback(forceRound);
  210. }
  211. bool SpecialRow::useForumLikeUserpic() const {
  212. return true;
  213. }
  214. TypesController::TypesController(
  215. not_null<Main::Session*> session,
  216. SpecialRowType type)
  217. : _session(session)
  218. , _type(type) {
  219. }
  220. Main::Session &TypesController::session() const {
  221. return *_session;
  222. }
  223. void TypesController::prepare() {
  224. delegate()->peerListAppendRow(std::make_unique<SpecialRow>(_type));
  225. delegate()->peerListRefreshRows();
  226. }
  227. bool TypesController::specialSelected() const {
  228. const auto premiums = (_type == SpecialRowType::Premiums);
  229. const auto row = delegate()->peerListFindRow(premiums
  230. ? kPremiumsRowId
  231. : kMiniAppsRowId);
  232. Assert(row != nullptr);
  233. return row->checked();
  234. }
  235. void TypesController::rowClicked(not_null<PeerListRow*> row) {
  236. const auto checked = !row->checked();
  237. delegate()->peerListSetRowChecked(row, checked);
  238. _rowSelectionChanges.fire({ row, checked });
  239. }
  240. rpl::producer<bool> TypesController::specialChanges() const {
  241. return _rowSelectionChanges.events(
  242. ) | rpl::map([=] {
  243. return specialSelected();
  244. });
  245. }
  246. auto TypesController::rowSelectionChanges() const
  247. -> rpl::producer<RowSelectionChange> {
  248. return _rowSelectionChanges.events();
  249. }
  250. PrivacyExceptionsBoxController::PrivacyExceptionsBoxController(
  251. not_null<Main::Session*> session,
  252. rpl::producer<QString> title,
  253. const Exceptions &selected,
  254. std::optional<SpecialRowType> allowChooseSpecial)
  255. : ChatsListBoxController(session)
  256. , _session(session)
  257. , _title(std::move(title))
  258. , _selected(selected)
  259. , _allowChooseSpecial(allowChooseSpecial) {
  260. }
  261. Main::Session &PrivacyExceptionsBoxController::session() const {
  262. return *_session;
  263. }
  264. void PrivacyExceptionsBoxController::prepareViewHook() {
  265. delegate()->peerListSetTitle(std::move(_title));
  266. if (_allowChooseSpecial || _selected.premiums || _selected.miniapps) {
  267. delegate()->peerListSetAboveWidget(prepareSpecialRowList(
  268. _allowChooseSpecial.value_or(_selected.premiums
  269. ? SpecialRowType::Premiums
  270. : SpecialRowType::MiniApps)));
  271. }
  272. delegate()->peerListAddSelectedPeers(_selected.peers);
  273. }
  274. bool PrivacyExceptionsBoxController::isForeignRow(PeerListRowId itemId) {
  275. return (itemId == kPremiumsRowId)
  276. || (itemId == kMiniAppsRowId);
  277. }
  278. bool PrivacyExceptionsBoxController::handleDeselectForeignRow(
  279. PeerListRowId itemId) {
  280. if (isForeignRow(itemId)) {
  281. _deselectOption(itemId);
  282. return true;
  283. }
  284. return false;
  285. }
  286. auto PrivacyExceptionsBoxController::prepareSpecialRowList(
  287. SpecialRowType type)
  288. -> object_ptr<Ui::RpWidget> {
  289. auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
  290. const auto container = result.data();
  291. container->add(CreatePeerListSectionSubtitle(
  292. container,
  293. tr::lng_edit_privacy_user_types()));
  294. auto &lifetime = container->lifetime();
  295. _typesDelegate = lifetime.make_state<PeerListContentDelegateSimple>();
  296. const auto controller = lifetime.make_state<TypesController>(
  297. &session(),
  298. type);
  299. const auto content = result->add(object_ptr<PeerListContent>(
  300. container,
  301. controller));
  302. _typesDelegate->setContent(content);
  303. controller->setDelegate(_typesDelegate);
  304. const auto selectType = [&](PeerListRowId id) {
  305. const auto row = _typesDelegate->peerListFindRow(id);
  306. if (row) {
  307. content->changeCheckState(row, true, anim::type::instant);
  308. this->delegate()->peerListSetForeignRowChecked(
  309. row,
  310. true,
  311. anim::type::instant);
  312. }
  313. };
  314. if (_selected.premiums) {
  315. selectType(kPremiumsRowId);
  316. } else if (_selected.miniapps) {
  317. selectType(kMiniAppsRowId);
  318. }
  319. container->add(CreatePeerListSectionSubtitle(
  320. container,
  321. tr::lng_edit_privacy_users_and_groups()));
  322. controller->specialChanges(
  323. ) | rpl::start_with_next([=](bool chosen) {
  324. if (type == SpecialRowType::Premiums) {
  325. _selected.premiums = chosen;
  326. } else {
  327. _selected.miniapps = chosen;
  328. }
  329. }, lifetime);
  330. controller->rowSelectionChanges(
  331. ) | rpl::start_with_next([=](RowSelectionChange update) {
  332. this->delegate()->peerListSetForeignRowChecked(
  333. update.row,
  334. update.checked,
  335. anim::type::normal);
  336. }, lifetime);
  337. _deselectOption = [=](PeerListRowId itemId) {
  338. if (const auto row = _typesDelegate->peerListFindRow(itemId)) {
  339. if (itemId == kPremiumsRowId) {
  340. _selected.premiums = false;
  341. } else if (itemId == kMiniAppsRowId) {
  342. _selected.miniapps = false;
  343. }
  344. _typesDelegate->peerListSetRowChecked(row, false);
  345. }
  346. };
  347. return result;
  348. }
  349. bool PrivacyExceptionsBoxController::premiumsSelected() const {
  350. return _selected.premiums;
  351. }
  352. bool PrivacyExceptionsBoxController::miniAppsSelected() const {
  353. return _selected.miniapps;
  354. }
  355. void PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {
  356. const auto peer = row->peer();
  357. // This call may delete row, if it was a search result row.
  358. delegate()->peerListSetRowChecked(row, !row->checked());
  359. if (const auto channel = peer->asChannel()) {
  360. if (!channel->membersCountKnown()) {
  361. channel->updateFull();
  362. }
  363. }
  364. }
  365. auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
  366. -> std::unique_ptr<Row> {
  367. const auto peer = history->peer;
  368. if (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {
  369. return nullptr;
  370. } else if (!peer->isUser()
  371. && !peer->isChat()
  372. && !peer->isMegagroup()) {
  373. return nullptr;
  374. }
  375. auto result = std::make_unique<Row>(history);
  376. const auto count = [&] {
  377. if (const auto chat = history->peer->asChat()) {
  378. return chat->count;
  379. } else if (const auto channel = history->peer->asChannel()) {
  380. return channel->membersCountKnown()
  381. ? channel->membersCount()
  382. : 0;
  383. }
  384. return 0;
  385. }();
  386. if (count > 0) {
  387. result->setCustomStatus(
  388. tr::lng_chat_status_members(tr::now, lt_count_decimal, count));
  389. }
  390. return result;
  391. }
  392. [[nodiscard]] object_ptr<Ui::RpWidget> MakeChargeStarsSlider(
  393. QWidget *parent,
  394. not_null<const style::MediaSlider*> sliderStyle,
  395. not_null<const style::FlatLabel*> labelStyle,
  396. int valuesCount,
  397. Fn<int(int)> valueByIndex,
  398. int value,
  399. int maxValue,
  400. Fn<void(int)> valueProgress,
  401. Fn<void(int)> valueFinished) {
  402. auto result = object_ptr<Ui::VerticalLayout>(parent);
  403. const auto raw = result.data();
  404. const auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));
  405. const auto min = Ui::CreateChild<Ui::FlatLabel>(
  406. raw,
  407. QString::number(kStarsMin),
  408. *labelStyle);
  409. const auto max = Ui::CreateChild<Ui::FlatLabel>(
  410. raw,
  411. QString::number(maxValue),
  412. *labelStyle);
  413. const auto current = Ui::CreateChild<Ui::FlatLabel>(
  414. raw,
  415. QString::number(value),
  416. *labelStyle);
  417. min->setTextColorOverride(st::windowSubTextFg->c);
  418. max->setTextColorOverride(st::windowSubTextFg->c);
  419. const auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(
  420. raw,
  421. *sliderStyle));
  422. labels->resize(
  423. labels->width(),
  424. current->height() + st::defaultVerticalListSkip);
  425. struct State {
  426. int indexMin = 0;
  427. int index = 0;
  428. };
  429. const auto state = raw->lifetime().make_state<State>();
  430. const auto updateByIndex = [=] {
  431. const auto outer = labels->width();
  432. const auto minWidth = min->width();
  433. const auto maxWidth = max->width();
  434. const auto currentWidth = current->width();
  435. if (minWidth + maxWidth + currentWidth > outer) {
  436. return;
  437. }
  438. min->moveToLeft(0, 0, outer);
  439. max->moveToRight(0, 0, outer);
  440. current->moveToLeft((outer - current->width()) / 2, 0, outer);
  441. };
  442. const auto updateByValue = [=](int value) {
  443. current->setText(
  444. tr::lng_action_gift_for_stars(tr::now, lt_count, value));
  445. state->index = 0;
  446. auto maxIndex = valuesCount - 1;
  447. while (state->index < maxIndex) {
  448. const auto mid = (state->index + maxIndex) / 2;
  449. const auto midValue = valueByIndex(mid);
  450. if (midValue == value) {
  451. state->index = mid;
  452. break;
  453. } else if (midValue < value) {
  454. state->index = mid + 1;
  455. } else {
  456. maxIndex = mid - 1;
  457. }
  458. }
  459. updateByIndex();
  460. };
  461. const auto progress = [=](int value) {
  462. updateByValue(value);
  463. valueProgress(value);
  464. };
  465. const auto finished = [=](int value) {
  466. updateByValue(value);
  467. valueFinished(value);
  468. };
  469. style::PaletteChanged() | rpl::start_with_next([=] {
  470. min->setTextColorOverride(st::windowSubTextFg->c);
  471. max->setTextColorOverride(st::windowSubTextFg->c);
  472. }, raw->lifetime());
  473. updateByValue(value);
  474. state->indexMin = 0;
  475. slider->setPseudoDiscrete(
  476. valuesCount,
  477. valueByIndex,
  478. value,
  479. progress,
  480. finished,
  481. state->indexMin);
  482. slider->resize(slider->width(), sliderStyle->seekSize.height());
  483. raw->widthValue() | rpl::start_with_next([=](int width) {
  484. labels->resizeToWidth(width);
  485. updateByIndex();
  486. }, slider->lifetime());
  487. return result;
  488. }
  489. void EditNoPaidMessagesExceptions(
  490. not_null<Window::SessionController*> window,
  491. const Api::UserPrivacy::Rule &value) {
  492. auto controller = std::make_unique<PrivacyExceptionsBoxController>(
  493. &window->session(),
  494. tr::lng_messages_privacy_remove_fee(),
  495. value.always,
  496. std::optional<SpecialRowType>());
  497. auto initBox = [=, controller = controller.get()](
  498. not_null<PeerListBox*> box) {
  499. box->addButton(tr::lng_settings_save(), [=] {
  500. auto copy = value;
  501. auto &setTo = copy.always;
  502. setTo.peers = box->collectSelectedRows();
  503. setTo.premiums = false;
  504. setTo.miniapps = false;
  505. auto &removeFrom = copy.never;
  506. for (const auto peer : setTo.peers) {
  507. removeFrom.peers.erase(
  508. ranges::remove(removeFrom.peers, peer),
  509. end(removeFrom.peers));
  510. }
  511. window->session().api().userPrivacy().save(
  512. Api::UserPrivacy::Key::NoPaidMessages,
  513. copy);
  514. box->closeBox();
  515. });
  516. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  517. };
  518. window->show(
  519. Box<PeerListBox>(std::move(controller), std::move(initBox)));
  520. }
  521. } // namespace
  522. bool EditPrivacyController::hasOption(Option option) const {
  523. return (option != Option::CloseFriends);
  524. }
  525. QString EditPrivacyController::optionLabel(Option option) const {
  526. switch (option) {
  527. case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now);
  528. case Option::Contacts: return tr::lng_edit_privacy_contacts(tr::now);
  529. case Option::CloseFriends:
  530. return tr::lng_edit_privacy_close_friends(tr::now);
  531. case Option::Nobody: return tr::lng_edit_privacy_nobody(tr::now);
  532. }
  533. Unexpected("Option value in optionsLabelKey.");
  534. }
  535. EditPrivacyBox::EditPrivacyBox(
  536. QWidget*,
  537. not_null<Window::SessionController*> window,
  538. std::unique_ptr<EditPrivacyController> controller,
  539. const Value &value)
  540. : _window(window)
  541. , _controller(std::move(controller))
  542. , _value(value) {
  543. if (_controller->allowPremiumsToggle(Exception::Always)
  544. && _value.option == Option::Everyone) {
  545. // If we switch from Everyone to Contacts or Nobody suggest Premiums.
  546. _value.always.premiums = true;
  547. }
  548. if (_controller->allowMiniAppsToggle(Exception::Always)
  549. && _value.option == Option::Everyone) {
  550. // If we switch from Everyone to Contacts or Nobody suggest MiniApps.
  551. _value.always.miniapps = true;
  552. }
  553. }
  554. void EditPrivacyBox::prepare() {
  555. _controller->setView(this);
  556. setupContent();
  557. }
  558. void EditPrivacyBox::editExceptions(
  559. Exception exception,
  560. Fn<void()> done) {
  561. auto controller = std::make_unique<PrivacyExceptionsBoxController>(
  562. &_window->session(),
  563. _controller->exceptionBoxTitle(exception),
  564. exceptions(exception),
  565. (_controller->allowPremiumsToggle(exception)
  566. ? SpecialRowType::Premiums
  567. : _controller->allowMiniAppsToggle(exception)
  568. ? SpecialRowType::MiniApps
  569. : std::optional<SpecialRowType>()));
  570. auto initBox = [=, controller = controller.get()](
  571. not_null<PeerListBox*> box) {
  572. box->addButton(tr::lng_settings_save(), crl::guard(this, [=] {
  573. auto &setTo = exceptions(exception);
  574. setTo.peers = box->collectSelectedRows();
  575. setTo.premiums = controller->premiumsSelected();
  576. setTo.miniapps = controller->miniAppsSelected();
  577. const auto type = [&] {
  578. switch (exception) {
  579. case Exception::Always: return Exception::Never;
  580. case Exception::Never: return Exception::Always;
  581. }
  582. Unexpected("Invalid exception value.");
  583. }();
  584. auto &removeFrom = exceptions(type);
  585. for (const auto peer : exceptions(exception).peers) {
  586. removeFrom.peers.erase(
  587. ranges::remove(removeFrom.peers, peer),
  588. end(removeFrom.peers));
  589. }
  590. if (setTo.premiums) {
  591. removeFrom.premiums = false;
  592. }
  593. if (setTo.miniapps) {
  594. removeFrom.miniapps = false;
  595. }
  596. done();
  597. box->closeBox();
  598. }));
  599. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  600. };
  601. _window->show(
  602. Box<PeerListBox>(std::move(controller), std::move(initBox)));
  603. }
  604. EditPrivacyBox::Exceptions &EditPrivacyBox::exceptions(Exception exception) {
  605. switch (exception) {
  606. case Exception::Always: return _value.always;
  607. case Exception::Never: return _value.never;
  608. }
  609. Unexpected("Invalid exception value.");
  610. }
  611. bool EditPrivacyBox::showExceptionLink(Exception exception) const {
  612. switch (exception) {
  613. case Exception::Always:
  614. return (_value.option == Option::Contacts)
  615. || (_value.option == Option::CloseFriends)
  616. || (_value.option == Option::Nobody);
  617. case Exception::Never:
  618. return (_value.option == Option::Everyone)
  619. || (_value.option == Option::Contacts)
  620. || (_value.option == Option::CloseFriends);
  621. }
  622. Unexpected("Invalid exception value.");
  623. }
  624. Ui::Radioenum<EditPrivacyBox::Option> *EditPrivacyBox::AddOption(
  625. not_null<Ui::VerticalLayout*> container,
  626. not_null<EditPrivacyController*> controller,
  627. const std::shared_ptr<Ui::RadioenumGroup<Option>> &group,
  628. Option option) {
  629. return container->add(
  630. object_ptr<Ui::Radioenum<Option>>(
  631. container,
  632. group,
  633. option,
  634. controller->optionLabel(option),
  635. st::settingsPrivacyOption),
  636. (st::settingsSendTypePadding + style::margins(
  637. -st::lineWidth,
  638. st::settingsPrivacySkipTop,
  639. 0,
  640. 0)));
  641. }
  642. Ui::FlatLabel *EditPrivacyBox::addLabel(
  643. not_null<Ui::VerticalLayout*> container,
  644. rpl::producer<TextWithEntities> text,
  645. int topSkip) {
  646. if (!text) {
  647. return nullptr;
  648. }
  649. auto label = object_ptr<Ui::FlatLabel>(
  650. container,
  651. rpl::duplicate(text),
  652. st::boxDividerLabel);
  653. const auto result = label.data();
  654. container->add(
  655. object_ptr<Ui::DividerLabel>(
  656. container,
  657. std::move(label),
  658. st::defaultBoxDividerLabelPadding),
  659. { 0, topSkip, 0, 0 });
  660. return result;
  661. }
  662. Ui::FlatLabel *EditPrivacyBox::addLabelOrDivider(
  663. not_null<Ui::VerticalLayout*> container,
  664. rpl::producer<TextWithEntities> text,
  665. int topSkip) {
  666. if (const auto result = addLabel(container, std::move(text), topSkip)) {
  667. return result;
  668. }
  669. container->add(
  670. object_ptr<Ui::BoxContentDivider>(container),
  671. { 0, topSkip, 0, 0 });
  672. return nullptr;
  673. }
  674. void EditPrivacyBox::setupContent() {
  675. using namespace Settings;
  676. setTitle(_controller->title());
  677. auto wrap = object_ptr<Ui::VerticalLayout>(this);
  678. const auto content = wrap.data();
  679. setInnerWidget(object_ptr<Ui::OverrideMargins>(
  680. this,
  681. std::move(wrap)));
  682. const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
  683. _value.option);
  684. const auto toggle = Ui::CreateChild<rpl::event_stream<Option>>(content);
  685. group->setChangedCallback([=](Option value) {
  686. _value.option = value;
  687. toggle->fire_copy(value);
  688. });
  689. auto optionValue = toggle->events_starting_with_copy(_value.option);
  690. const auto addOptionRow = [&](Option option) {
  691. return (_controller->hasOption(option) || (_value.option == option))
  692. ? AddOption(content, _controller.get(), group, option)
  693. : nullptr;
  694. };
  695. const auto addExceptionLink = [=](Exception exception) {
  696. const auto update = Ui::CreateChild<rpl::event_stream<>>(content);
  697. auto label = update->events_starting_with({}) | rpl::map([=] {
  698. const auto &value = exceptions(exception);
  699. const auto count = Settings::ExceptionUsersCount(value.peers);
  700. const auto users = count
  701. ? tr::lng_edit_privacy_exceptions_count(
  702. tr::now,
  703. lt_count,
  704. count)
  705. : tr::lng_edit_privacy_exceptions_add(tr::now);
  706. return value.premiums
  707. ? (!count
  708. ? tr::lng_edit_privacy_premium(tr::now)
  709. : tr::lng_edit_privacy_exceptions_premium_and(
  710. tr::now,
  711. lt_users,
  712. users))
  713. : value.miniapps
  714. ? (!count
  715. ? tr::lng_edit_privacy_miniapps(tr::now)
  716. : tr::lng_edit_privacy_exceptions_miniapps_and(
  717. tr::now,
  718. lt_users,
  719. users))
  720. : users;
  721. });
  722. _controller->handleExceptionsChange(
  723. exception,
  724. update->events_starting_with({}) | rpl::map([=] {
  725. return Settings::ExceptionUsersCount(
  726. exceptions(exception).peers);
  727. }));
  728. auto text = _controller->exceptionButtonTextKey(exception);
  729. const auto button = content->add(
  730. object_ptr<Ui::SlideWrap<Button>>(
  731. content,
  732. object_ptr<Button>(
  733. content,
  734. rpl::duplicate(text),
  735. st::settingsButtonNoIcon)));
  736. CreateRightLabel(
  737. button->entity(),
  738. std::move(label),
  739. st::settingsButtonNoIcon,
  740. std::move(text));
  741. button->toggleOn(rpl::duplicate(
  742. optionValue
  743. ) | rpl::map([=] {
  744. return showExceptionLink(exception);
  745. }))->entity()->addClickHandler([=] {
  746. editExceptions(exception, [=] { update->fire({}); });
  747. });
  748. return button;
  749. };
  750. auto above = _controller->setupAboveWidget(
  751. _window,
  752. content,
  753. rpl::duplicate(optionValue),
  754. getDelegate()->outerContainer());
  755. if (above) {
  756. content->add(std::move(above));
  757. }
  758. Ui::AddSubsectionTitle(
  759. content,
  760. _controller->optionsTitleKey(),
  761. { 0, st::settingsPrivacySkipTop, 0, 0 });
  762. const auto options = {
  763. Option::Everyone,
  764. Option::Contacts,
  765. Option::CloseFriends,
  766. Option::Nobody,
  767. };
  768. for (const auto &option : options) {
  769. if (const auto row = addOptionRow(option)) {
  770. const auto premiumCallback = _controller->premiumClickedCallback(
  771. option,
  772. _window);
  773. if (premiumCallback) {
  774. AddPremiumRequiredRow(
  775. row,
  776. &_window->session(),
  777. premiumCallback,
  778. [=] { group->setValue(Option::Everyone); },
  779. st::messagePrivacyCheck);
  780. }
  781. }
  782. }
  783. const auto warning = addLabelOrDivider(
  784. content,
  785. _controller->warning(),
  786. st::defaultVerticalListSkip + st::settingsPrivacySkipTop);
  787. if (warning) {
  788. _controller->prepareWarningLabel(warning);
  789. }
  790. auto middle = _controller->setupMiddleWidget(
  791. _window,
  792. content,
  793. rpl::duplicate(optionValue));
  794. if (middle) {
  795. content->add(std::move(middle));
  796. }
  797. Ui::AddSkip(content);
  798. Ui::AddSubsectionTitle(
  799. content,
  800. tr::lng_edit_privacy_exceptions(),
  801. { 0, st::settingsPrivacySkipTop, 0, 0 });
  802. const auto always = addExceptionLink(Exception::Always);
  803. const auto never = addExceptionLink(Exception::Never);
  804. addLabel(
  805. content,
  806. _controller->exceptionsDescription() | Ui::Text::ToWithEntities(),
  807. st::defaultVerticalListSkip);
  808. auto below = _controller->setupBelowWidget(
  809. _window,
  810. content,
  811. rpl::duplicate(optionValue));
  812. if (below) {
  813. content->add(std::move(below));
  814. }
  815. addButton(tr::lng_settings_save(), [=] {
  816. const auto someAreDisallowed = (_value.option != Option::Everyone)
  817. || !_value.never.peers.empty();
  818. _controller->confirmSave(someAreDisallowed, crl::guard(this, [=] {
  819. _value.ignoreAlways = !showExceptionLink(Exception::Always);
  820. _value.ignoreNever = !showExceptionLink(Exception::Never);
  821. _controller->saveAdditional();
  822. _window->session().api().userPrivacy().save(
  823. _controller->key(),
  824. _value);
  825. closeBox();
  826. }));
  827. });
  828. addButton(tr::lng_cancel(), [this] { closeBox(); });
  829. const auto linkHeight = st::settingsButtonNoIcon.padding.top()
  830. + st::settingsButtonNoIcon.height
  831. + st::settingsButtonNoIcon.padding.bottom();
  832. widthValue(
  833. ) | rpl::start_with_next([=](int width) {
  834. content->resizeToWidth(width);
  835. }, content->lifetime());
  836. content->heightValue(
  837. ) | rpl::map([=](int height) {
  838. return height - always->height() - never->height() + 2 * linkHeight;
  839. }) | rpl::distinct_until_changed(
  840. ) | rpl::start_with_next([=](int height) {
  841. setDimensions(st::boxWideWidth, height);
  842. }, content->lifetime());
  843. }
  844. void EditMessagesPrivacyBox(
  845. not_null<Ui::GenericBox*> box,
  846. not_null<Window::SessionController*> controller) {
  847. box->setTitle(tr::lng_messages_privacy_title());
  848. box->setWidth(st::boxWideWidth);
  849. constexpr auto kOptionAll = 0;
  850. constexpr auto kOptionPremium = 1;
  851. constexpr auto kOptionCharge = 2;
  852. const auto session = &controller->session();
  853. const auto allowed = [=] {
  854. return session->premium()
  855. || session->appConfig().newRequirePremiumFree();
  856. };
  857. const auto privacy = &session->api().globalPrivacy();
  858. const auto inner = box->verticalLayout();
  859. inner->add(object_ptr<Ui::PlainShadow>(box));
  860. Ui::AddSkip(inner, st::messagePrivacyTopSkip);
  861. Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle());
  862. const auto group = std::make_shared<Ui::RadiobuttonGroup>(
  863. (!allowed()
  864. ? kOptionAll
  865. : privacy->newRequirePremiumCurrent()
  866. ? kOptionPremium
  867. : privacy->newChargeStarsCurrent()
  868. ? kOptionCharge
  869. : kOptionAll));
  870. inner->add(
  871. object_ptr<Ui::Radiobutton>(
  872. inner,
  873. group,
  874. kOptionAll,
  875. tr::lng_messages_privacy_everyone(tr::now),
  876. st::messagePrivacyCheck),
  877. st::settingsSendTypePadding);
  878. const auto restricted = inner->add(
  879. object_ptr<Ui::Radiobutton>(
  880. inner,
  881. group,
  882. kOptionPremium,
  883. tr::lng_messages_privacy_restricted(tr::now),
  884. st::messagePrivacyCheck),
  885. st::settingsSendTypePadding + style::margins(
  886. 0,
  887. st::messagePrivacyRadioSkip,
  888. 0,
  889. st::messagePrivacyBottomSkip));
  890. Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
  891. const auto available = session->appConfig().paidMessagesAvailable();
  892. const auto charged = available
  893. ? inner->add(
  894. object_ptr<Ui::Radiobutton>(
  895. inner,
  896. group,
  897. kOptionCharge,
  898. tr::lng_messages_privacy_charge(tr::now),
  899. st::messagePrivacyCheck),
  900. st::settingsSendTypePadding + style::margins(
  901. 0,
  902. st::messagePrivacyBottomSkip,
  903. 0,
  904. st::messagePrivacyBottomSkip))
  905. : nullptr;
  906. struct State {
  907. rpl::variable<int> stars;
  908. };
  909. const auto state = std::make_shared<State>();
  910. const auto savedValue = privacy->newChargeStarsCurrent();
  911. if (available) {
  912. Ui::AddDividerText(inner, tr::lng_messages_privacy_charge_about());
  913. const auto chargeWrap = inner->add(
  914. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  915. inner,
  916. object_ptr<Ui::VerticalLayout>(inner)));
  917. const auto chargeInner = chargeWrap->entity();
  918. Ui::AddSkip(chargeInner);
  919. state->stars = SetupChargeSlider(
  920. chargeInner,
  921. session->user(),
  922. savedValue);
  923. Ui::AddSkip(chargeInner);
  924. Ui::AddSubsectionTitle(
  925. chargeInner,
  926. tr::lng_messages_privacy_exceptions());
  927. const auto key = Api::UserPrivacy::Key::NoPaidMessages;
  928. session->api().userPrivacy().reload(key);
  929. auto label = session->api().userPrivacy().value(
  930. key
  931. ) | rpl::map([=](const Api::UserPrivacy::Rule &value) {
  932. using namespace Settings;
  933. const auto always = ExceptionUsersCount(value.always.peers);
  934. return always
  935. ? tr::lng_edit_privacy_exceptions_count(
  936. tr::now,
  937. lt_count,
  938. always)
  939. : tr::lng_edit_privacy_exceptions_add(tr::now);
  940. });
  941. const auto exceptions = Settings::AddButtonWithLabel(
  942. chargeInner,
  943. tr::lng_messages_privacy_remove_fee(),
  944. std::move(label),
  945. st::settingsButtonNoIcon);
  946. const auto shower = exceptions->lifetime().make_state<rpl::lifetime>();
  947. exceptions->setClickedCallback([=] {
  948. *shower = session->api().userPrivacy().value(
  949. key
  950. ) | rpl::take(
  951. 1
  952. ) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) {
  953. EditNoPaidMessagesExceptions(controller, value);
  954. });
  955. });
  956. Ui::AddSkip(chargeInner);
  957. Ui::AddDividerText(
  958. chargeInner,
  959. tr::lng_messages_privacy_remove_about());
  960. using namespace rpl::mappers;
  961. chargeWrap->toggleOn(group->value() | rpl::map(_1 == kOptionCharge));
  962. chargeWrap->finishAnimating();
  963. }
  964. using WeakToast = base::weak_ptr<Ui::Toast::Instance>;
  965. const auto toast = std::make_shared<WeakToast>();
  966. const auto showToast = [=] {
  967. auto link = Ui::Text::Link(
  968. Ui::Text::Semibold(
  969. tr::lng_messages_privacy_premium_link(tr::now)));
  970. (*toast) = controller->showToast({
  971. .text = tr::lng_messages_privacy_premium(
  972. tr::now,
  973. lt_link,
  974. link,
  975. Ui::Text::WithEntities),
  976. .filter = crl::guard(&controller->session(), [=](
  977. const ClickHandlerPtr &,
  978. Qt::MouseButton button) {
  979. if (button == Qt::LeftButton) {
  980. if (const auto strong = toast->get()) {
  981. strong->hideAnimated();
  982. (*toast) = nullptr;
  983. Settings::ShowPremium(
  984. controller,
  985. u"noncontact_peers_require_premium"_q);
  986. return true;
  987. }
  988. }
  989. return false;
  990. }),
  991. });
  992. };
  993. if (!allowed()) {
  994. CreateRadiobuttonLock(restricted, st::messagePrivacyCheck);
  995. if (charged) {
  996. CreateRadiobuttonLock(charged, st::messagePrivacyCheck);
  997. }
  998. group->setChangedCallback([=](int value) {
  999. if (value == kOptionPremium || value == kOptionCharge) {
  1000. group->setValue(kOptionAll);
  1001. showToast();
  1002. }
  1003. });
  1004. Ui::AddSkip(inner);
  1005. Settings::AddButtonWithIcon(
  1006. inner,
  1007. tr::lng_messages_privacy_premium_button(),
  1008. st::messagePrivacySubscribe,
  1009. { .icon = &st::menuBlueIconPremium }
  1010. )->setClickedCallback([=] {
  1011. Settings::ShowPremium(
  1012. controller,
  1013. u"noncontact_peers_require_premium"_q);
  1014. });
  1015. Ui::AddSkip(inner);
  1016. Ui::AddDividerText(inner, tr::lng_messages_privacy_premium_about());
  1017. box->addButton(tr::lng_about_done(), [=] {
  1018. box->closeBox();
  1019. });
  1020. } else {
  1021. box->addButton(tr::lng_settings_save(), [=] {
  1022. if (allowed()) {
  1023. const auto value = group->current();
  1024. const auto premiumRequired = (value == kOptionPremium);
  1025. const auto chargeStars = (value == kOptionCharge)
  1026. ? state->stars.current()
  1027. : 0;
  1028. privacy->updateMessagesPrivacy(premiumRequired, chargeStars);
  1029. box->closeBox();
  1030. } else {
  1031. showToast();
  1032. }
  1033. });
  1034. box->addButton(tr::lng_cancel(), [=] {
  1035. box->closeBox();
  1036. });
  1037. }
  1038. }
  1039. rpl::producer<int> SetupChargeSlider(
  1040. not_null<Ui::VerticalLayout*> container,
  1041. not_null<PeerData*> peer,
  1042. int savedValue) {
  1043. struct State {
  1044. rpl::variable<int> stars;
  1045. };
  1046. const auto group = !peer->isUser();
  1047. const auto state = container->lifetime().make_state<State>();
  1048. const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars;
  1049. state->stars = chargeStars;
  1050. Ui::AddSubsectionTitle(container, group
  1051. ? tr::lng_rights_charge_price()
  1052. : tr::lng_messages_privacy_price());
  1053. auto values = std::vector<int>();
  1054. const auto maxStars = peer->session().appConfig().paidMessageStarsMax();
  1055. if (chargeStars < kStarsMin) {
  1056. values.push_back(chargeStars);
  1057. }
  1058. for (auto i = kStarsMin; i < std::min(100, maxStars); ++i) {
  1059. values.push_back(i);
  1060. }
  1061. for (auto i = 100; i < std::min(1000, maxStars); i += 10) {
  1062. if (i < chargeStars + 10 && chargeStars < i) {
  1063. values.push_back(chargeStars);
  1064. }
  1065. values.push_back(i);
  1066. }
  1067. for (auto i = 1000; i < maxStars + 1; i += 100) {
  1068. if (i < chargeStars + 100 && chargeStars < i) {
  1069. values.push_back(chargeStars);
  1070. }
  1071. values.push_back(i);
  1072. }
  1073. const auto valuesCount = int(values.size());
  1074. const auto setStars = [=](int value) {
  1075. state->stars = value;
  1076. };
  1077. container->add(
  1078. MakeChargeStarsSlider(
  1079. container,
  1080. &st::settingsScale,
  1081. &st::settingsScaleLabel,
  1082. valuesCount,
  1083. [=](int index) { return values[index]; },
  1084. chargeStars,
  1085. maxStars,
  1086. setStars,
  1087. setStars),
  1088. st::boxRowPadding);
  1089. const auto skip = 2 * st::defaultVerticalListSkip;
  1090. Ui::AddSkip(container, skip);
  1091. auto dollars = state->stars.value() | rpl::map([=](int stars) {
  1092. const auto ratio = peer->session().appConfig().starsWithdrawRate();
  1093. const auto dollars = int(base::SafeRound(stars * ratio));
  1094. return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q);
  1095. });
  1096. const auto percent = peer->session().appConfig().paidMessageCommission();
  1097. Ui::AddDividerText(
  1098. container,
  1099. (group
  1100. ? tr::lng_rights_charge_price_about
  1101. : tr::lng_messages_privacy_price_about)(
  1102. lt_percent,
  1103. rpl::single(QString::number(percent / 10.) + '%'),
  1104. lt_amount,
  1105. std::move(dollars)));
  1106. return state->stars.value();
  1107. }