media_stories_stealth.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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 "media/stories/media_stories_stealth.h"
  8. #include "base/timer_rpl.h"
  9. #include "base/unixtime.h"
  10. #include "boxes/premium_preview_box.h"
  11. #include "chat_helpers/compose/compose_show.h"
  12. #include "data/data_peer_values.h"
  13. #include "data/data_session.h"
  14. #include "data/data_stories.h"
  15. #include "info/profile/info_profile_icon.h"
  16. #include "lang/lang_keys.h"
  17. #include "main/main_session.h"
  18. #include "settings/settings_premium.h"
  19. #include "ui/layers/generic_box.h"
  20. #include "ui/text/text_utilities.h"
  21. #include "ui/toast/toast.h"
  22. #include "ui/widgets/buttons.h"
  23. #include "ui/painter.h"
  24. #include "window/window_controller.h"
  25. #include "window/window_session_controller.h"
  26. #include "styles/style_media_view.h"
  27. #include "styles/style_layers.h"
  28. namespace Media::Stories {
  29. namespace {
  30. constexpr auto kAlreadyToastDuration = 4 * crl::time(1000);
  31. constexpr auto kCooldownButtonLabelOpacity = 0.5;
  32. struct State {
  33. Data::StealthMode mode;
  34. TimeId now = 0;
  35. bool premium = false;
  36. };
  37. struct Feature {
  38. const style::icon &icon;
  39. QString title;
  40. TextWithEntities about;
  41. };
  42. [[nodiscard]] Ui::Toast::Config ToastAlready(TimeId left) {
  43. return {
  44. .title = tr::lng_stealth_mode_already_title(tr::now),
  45. .text = tr::lng_stealth_mode_already_about(
  46. tr::now,
  47. lt_left,
  48. TextWithEntities{ TimeLeftText(left) },
  49. Ui::Text::RichLangValue),
  50. .st = &st::storiesStealthToast,
  51. .adaptive = true,
  52. .duration = kAlreadyToastDuration,
  53. };
  54. }
  55. [[nodiscard]] Ui::Toast::Config ToastActivated() {
  56. return {
  57. .title = tr::lng_stealth_mode_enabled_tip_title(tr::now),
  58. .text = tr::lng_stealth_mode_enabled_tip(
  59. tr::now,
  60. Ui::Text::RichLangValue),
  61. .st = &st::storiesStealthToast,
  62. .adaptive = true,
  63. .duration = kAlreadyToastDuration,
  64. };
  65. }
  66. [[nodiscard]] Ui::Toast::Config ToastCooldown() {
  67. return {
  68. .text = tr::lng_stealth_mode_cooldown_tip(
  69. tr::now,
  70. Ui::Text::RichLangValue),
  71. .st = &st::storiesStealthToast,
  72. .adaptive = true,
  73. .duration = kAlreadyToastDuration,
  74. };
  75. }
  76. [[nodiscard]] rpl::producer<State> StateValue(
  77. not_null<Main::Session*> session) {
  78. return rpl::combine(
  79. session->data().stories().stealthModeValue(),
  80. Data::AmPremiumValue(session)
  81. ) | rpl::map([](Data::StealthMode mode, bool premium) {
  82. return rpl::make_producer<State>([=](auto consumer) {
  83. struct Info {
  84. base::Timer timer;
  85. bool firstSent = false;
  86. bool enabledSent = false;
  87. bool cooldownSent = false;
  88. };
  89. auto lifetime = rpl::lifetime();
  90. const auto info = lifetime.make_state<Info>();
  91. const auto check = [=] {
  92. auto send = !info->firstSent;
  93. const auto now = base::unixtime::now();
  94. const auto left1 = (mode.enabledTill - now);
  95. const auto left2 = (mode.cooldownTill - now);
  96. info->firstSent = true;
  97. if (!info->enabledSent && left1 <= 0) {
  98. send = true;
  99. info->enabledSent = true;
  100. }
  101. if (!info->cooldownSent && left2 <= 0) {
  102. send = true;
  103. info->cooldownSent = true;
  104. }
  105. const auto left = (left1 <= 0)
  106. ? left2
  107. : (left2 <= 0)
  108. ? left1
  109. : std::min(left1, left2);
  110. if (left > 0) {
  111. info->timer.callOnce(left * crl::time(1000));
  112. }
  113. if (send) {
  114. consumer.put_next(State{ mode, now, premium });
  115. }
  116. if (left <= 0) {
  117. consumer.put_done();
  118. }
  119. };
  120. info->timer.setCallback(check);
  121. check();
  122. return lifetime;
  123. });
  124. }) | rpl::flatten_latest();
  125. }
  126. [[nodiscard]] Feature FeaturePast() {
  127. return {
  128. .icon = st::storiesStealthFeaturePastIcon,
  129. .title = tr::lng_stealth_mode_past_title(tr::now),
  130. .about = { tr::lng_stealth_mode_past_about(tr::now) },
  131. };
  132. }
  133. [[nodiscard]] Feature FeatureNext() {
  134. return {
  135. .icon = st::storiesStealthFeatureNextIcon,
  136. .title = tr::lng_stealth_mode_next_title(tr::now),
  137. .about = { tr::lng_stealth_mode_next_about(tr::now) },
  138. };
  139. }
  140. [[nodiscard]] object_ptr<Ui::RpWidget> MakeLogo(QWidget *parent) {
  141. const auto add = st::storiesStealthLogoAdd;
  142. const auto icon = &st::storiesStealthLogoIcon;
  143. const auto size = QSize(2 * add, 2 * add) + icon->size();
  144. auto result = object_ptr<Ui::PaddingWrap<Ui::RpWidget>>(
  145. parent,
  146. object_ptr<Ui::RpWidget>(parent),
  147. st::storiesStealthLogoMargin);
  148. const auto inner = result->entity();
  149. inner->resize(size);
  150. inner->paintRequest(
  151. ) | rpl::start_with_next([=] {
  152. auto p = QPainter(inner);
  153. auto hq = PainterHighQualityEnabler(p);
  154. p.setBrush(st::storiesComposeBlue);
  155. p.setPen(Qt::NoPen);
  156. const auto left = (inner->width() - size.width()) / 2;
  157. const auto top = (inner->height() - size.height()) / 2;
  158. const auto rect = QRect(QPoint(left, top), size);
  159. p.drawEllipse(rect);
  160. icon->paintInCenter(p, rect);
  161. }, inner->lifetime());
  162. return result;
  163. }
  164. [[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(QWidget *parent) {
  165. return object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
  166. parent,
  167. object_ptr<Ui::FlatLabel>(
  168. parent,
  169. tr::lng_stealth_mode_title(tr::now),
  170. st::storiesStealthBox.title),
  171. st::storiesStealthTitleMargin);
  172. }
  173. [[nodiscard]] object_ptr<Ui::RpWidget> MakeAbout(
  174. QWidget *parent,
  175. rpl::producer<State> state) {
  176. auto text = std::move(state) | rpl::map([](const State &state) {
  177. return state.premium
  178. ? tr::lng_stealth_mode_about(tr::now)
  179. : tr::lng_stealth_mode_unlock_about(tr::now);
  180. });
  181. return object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
  182. parent,
  183. object_ptr<Ui::FlatLabel>(
  184. parent,
  185. std::move(text),
  186. st::storiesStealthAbout),
  187. st::storiesStealthAboutMargin);
  188. }
  189. [[nodiscard]] object_ptr<Ui::RpWidget> MakeFeature(
  190. QWidget *parent,
  191. Feature feature) {
  192. auto result = object_ptr<Ui::PaddingWrap<>>(
  193. parent,
  194. object_ptr<Ui::RpWidget>(parent),
  195. st::storiesStealthFeatureMargin);
  196. const auto widget = result->entity();
  197. const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
  198. widget,
  199. feature.icon,
  200. st::storiesStealthFeatureIconPosition);
  201. const auto title = Ui::CreateChild<Ui::FlatLabel>(
  202. widget,
  203. feature.title,
  204. st::storiesStealthFeatureTitle);
  205. const auto about = Ui::CreateChild<Ui::FlatLabel>(
  206. widget,
  207. rpl::single(feature.about),
  208. st::storiesStealthFeatureAbout);
  209. icon->show();
  210. title->show();
  211. about->show();
  212. widget->widthValue(
  213. ) | rpl::start_with_next([=](int width) {
  214. const auto left = st::storiesStealthFeatureLabelLeft;
  215. const auto available = width - left;
  216. title->resizeToWidth(available);
  217. about->resizeToWidth(available);
  218. auto top = 0;
  219. title->move(left, top);
  220. top += title->height() + st::storiesStealthFeatureSkip;
  221. about->move(left, top);
  222. top += about->height();
  223. widget->resize(width, top);
  224. }, widget->lifetime());
  225. return result;
  226. }
  227. [[nodiscard]] object_ptr<Ui::RoundButton> MakeButton(
  228. QWidget *parent,
  229. rpl::producer<State> state) {
  230. auto text = rpl::duplicate(state) | rpl::map([](const State &state) {
  231. if (!state.premium) {
  232. return tr::lng_stealth_mode_unlock();
  233. } else if (state.mode.cooldownTill <= state.now) {
  234. return tr::lng_stealth_mode_enable();
  235. }
  236. return rpl::single(
  237. rpl::empty
  238. ) | rpl::then(
  239. base::timer_each(250)
  240. ) | rpl::map([=] {
  241. const auto now = base::unixtime::now();
  242. const auto left = std::max(state.mode.cooldownTill - now, 1);
  243. return tr::lng_stealth_mode_cooldown_in(
  244. tr::now,
  245. lt_left,
  246. TimeLeftText(left));
  247. }) | rpl::type_erased();
  248. }) | rpl::flatten_latest();
  249. auto result = object_ptr<Ui::RoundButton>(
  250. parent,
  251. rpl::single(QString()),
  252. st::storiesStealthBox.button);
  253. const auto raw = result.data();
  254. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  255. raw,
  256. std::move(text),
  257. st::storiesStealthButtonLabel);
  258. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  259. label->show();
  260. const auto lock = Ui::CreateChild<Ui::RpWidget>(raw);
  261. lock->setAttribute(Qt::WA_TransparentForMouseEvents);
  262. lock->resize(st::storiesStealthLockIcon.size());
  263. lock->paintRequest(
  264. ) | rpl::start_with_next([=] {
  265. auto p = QPainter(lock);
  266. st::storiesStealthLockIcon.paintInCenter(p, lock->rect());
  267. }, lock->lifetime());
  268. const auto lockLeft = -st::storiesStealthButtonLabel.style.font->height;
  269. const auto updateLabelLockGeometry = [=] {
  270. const auto outer = raw->width();
  271. const auto added = -st::storiesStealthBox.button.width;
  272. const auto skip = lock->isHidden() ? 0 : (lockLeft + lock->width());
  273. const auto width = outer - added - skip;
  274. const auto top = st::storiesStealthBox.button.textTop;
  275. label->resizeToWidth(width);
  276. label->move(added / 2, top);
  277. const auto inner = std::min(label->textMaxWidth(), width);
  278. const auto right = (added / 2) + (outer - inner) / 2 + inner;
  279. const auto lockTop = (label->height() - lock->height()) / 2;
  280. lock->move(right + lockLeft, top + lockTop);
  281. };
  282. std::move(state) | rpl::start_with_next([=](const State &state) {
  283. const auto cooldown = state.premium
  284. && (state.mode.cooldownTill > state.now);
  285. label->setOpacity(cooldown ? kCooldownButtonLabelOpacity : 1.);
  286. lock->setVisible(!state.premium);
  287. updateLabelLockGeometry();
  288. }, label->lifetime());
  289. raw->widthValue(
  290. ) | rpl::start_with_next(updateLabelLockGeometry, label->lifetime());
  291. return result;
  292. }
  293. [[nodiscard]] object_ptr<Ui::BoxContent> StealthModeBox(
  294. std::shared_ptr<ChatHelpers::Show> show) {
  295. return Box([=](not_null<Ui::GenericBox*> box) {
  296. struct Data {
  297. rpl::variable<State> state;
  298. bool requested = false;
  299. };
  300. const auto data = box->lifetime().make_state<Data>();
  301. data->state = StateValue(&show->session());
  302. box->setWidth(st::boxWideWidth);
  303. box->setStyle(st::storiesStealthBox);
  304. box->addRow(MakeLogo(box));
  305. box->addRow(MakeTitle(box));
  306. box->addRow(MakeAbout(box, data->state.value()));
  307. box->addRow(MakeFeature(box, FeaturePast()));
  308. box->addRow(
  309. MakeFeature(box, FeatureNext()),
  310. (st::boxRowPadding
  311. + QMargins(0, 0, 0, st::storiesStealthBoxBottom)));
  312. box->setNoContentMargin(true);
  313. box->addTopButton(st::storiesStealthBoxClose, [=] {
  314. box->closeBox();
  315. });
  316. const auto button = box->addButton(
  317. MakeButton(box, data->state.value()));
  318. button->resizeToWidth(st::boxWideWidth
  319. - st::storiesStealthBox.buttonPadding.left()
  320. - st::storiesStealthBox.buttonPadding.right());
  321. button->setClickedCallback([=] {
  322. const auto now = data->state.current();
  323. if (now.mode.enabledTill > now.now) {
  324. show->showToast(ToastActivated());
  325. box->closeBox();
  326. } else if (!now.premium) {
  327. data->requested = false;
  328. if (const auto window = show->resolveWindow()) {
  329. ShowPremiumPreviewBox(window, PremiumFeature::Stories);
  330. window->window().activate();
  331. }
  332. } else if (now.mode.cooldownTill > now.now) {
  333. show->showToast(ToastCooldown());
  334. box->closeBox();
  335. } else if (!data->requested) {
  336. data->requested = true;
  337. show->session().data().stories().activateStealthMode(
  338. crl::guard(box, [=] { data->requested = false; }));
  339. }
  340. });
  341. data->state.value() | rpl::filter([](const State &state) {
  342. return state.mode.enabledTill > state.now;
  343. }) | rpl::start_with_next([=] {
  344. box->closeBox();
  345. show->showToast(ToastActivated());
  346. }, box->lifetime());
  347. });
  348. }
  349. } // namespace
  350. void SetupStealthMode(std::shared_ptr<ChatHelpers::Show> show) {
  351. const auto now = base::unixtime::now();
  352. const auto mode = show->session().data().stories().stealthMode();
  353. if (const auto left = mode.enabledTill - now; left > 0) {
  354. show->showToast(ToastAlready(left));
  355. } else {
  356. show->show(StealthModeBox(show));
  357. }
  358. }
  359. QString TimeLeftText(int left) {
  360. Expects(left >= 0);
  361. const auto hours = left / 3600;
  362. const auto minutes = (left % 3600) / 60;
  363. const auto seconds = left % 60;
  364. const auto zero = QChar('0');
  365. if (hours) {
  366. return u"%1:%2:%3"_q
  367. .arg(hours)
  368. .arg(minutes, 2, 10, zero)
  369. .arg(seconds, 2, 10, zero);
  370. } else if (minutes) {
  371. return u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, zero);
  372. }
  373. return u"0:%1"_q.arg(left, 2, 10, zero);
  374. }
  375. } // namespace Media::Stories