/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_stealth.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_stories.h" #include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_premium.h" #include "ui/controls/feature_list.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/painter.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_media_view.h" #include "styles/style_media_stories.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" namespace Media::Stories { namespace { constexpr auto kAlreadyToastDuration = 4 * crl::time(1000); constexpr auto kCooldownButtonLabelOpacity = 0.5; struct State { Data::StealthMode mode; TimeId now = 0; bool premium = false; bool hasCallback = false; }; [[nodiscard]] Ui::Toast::Config ToastAlready(TimeId left) { return { .title = tr::lng_stealth_mode_already_title(tr::now), .text = tr::lng_stealth_mode_already_about( tr::now, lt_left, TextWithEntities{ TimeLeftText(left) }, tr::rich), .st = &st::storiesStealthToast, .adaptive = true, .duration = kAlreadyToastDuration, }; } [[nodiscard]] Ui::Toast::Config ToastActivated() { return { .title = tr::lng_stealth_mode_enabled_tip_title(tr::now), .text = tr::lng_stealth_mode_enabled_tip( tr::now, tr::rich), .st = &st::storiesStealthToast, .adaptive = true, .duration = kAlreadyToastDuration, }; } [[nodiscard]] Ui::Toast::Config ToastCooldown() { return { .text = tr::lng_stealth_mode_cooldown_tip( tr::now, tr::rich), .st = &st::storiesStealthToast, .adaptive = true, .duration = kAlreadyToastDuration, }; } [[nodiscard]] rpl::producer StateValue( not_null session, bool hasCallback = false) { return rpl::combine( session->data().stories().stealthModeValue(), Data::AmPremiumValue(session) ) | rpl::map([=](Data::StealthMode mode, bool premium) { return rpl::make_producer([=](auto consumer) { struct Info { base::Timer timer; bool firstSent = false; bool enabledSent = false; bool cooldownSent = false; }; auto lifetime = rpl::lifetime(); const auto info = lifetime.make_state(); const auto check = [=] { auto send = !info->firstSent; const auto now = base::unixtime::now(); const auto left1 = (mode.enabledTill - now); const auto left2 = (mode.cooldownTill - now); info->firstSent = true; if (!info->enabledSent && left1 <= 0) { send = true; info->enabledSent = true; } if (!info->cooldownSent && left2 <= 0) { send = true; info->cooldownSent = true; } const auto left = (left1 <= 0) ? left2 : (left2 <= 0) ? left1 : std::min(left1, left2); if (left > 0) { info->timer.callOnce(left * crl::time(1000)); } if (send) { consumer.put_next( State{ mode, now, premium, hasCallback }); } if (left <= 0) { consumer.put_done(); } }; info->timer.setCallback(check); check(); return lifetime; }); }) | rpl::flatten_latest(); } [[nodiscard]] Ui::FeatureListEntry FeaturePast( const style::StealthBoxStyle &st) { return { .icon = st.featurePastIcon, .title = tr::lng_stealth_mode_past_title(tr::now), .about = { tr::lng_stealth_mode_past_about(tr::now) }, }; } [[nodiscard]] Ui::FeatureListEntry FeatureNext( const style::StealthBoxStyle &st) { return { .icon = st.featureNextIcon, .title = tr::lng_stealth_mode_next_title(tr::now), .about = { tr::lng_stealth_mode_next_about(tr::now) }, }; } [[nodiscard]] object_ptr MakeLogo( QWidget *parent, const style::StealthBoxStyle &st) { const auto add = st::storiesStealthLogoAdd; const auto icon = &st.logoIcon; const auto size = QSize(2 * add, 2 * add) + icon->size(); auto result = object_ptr>( parent, object_ptr(parent), st::storiesStealthLogoMargin); const auto inner = result->entity(); inner->resize(size); inner->paintRequest( ) | rpl::on_next([=, &st] { auto p = QPainter(inner); auto hq = PainterHighQualityEnabler(p); p.setBrush(st.logoBg); p.setPen(Qt::NoPen); const auto left = (inner->width() - size.width()) / 2; const auto top = (inner->height() - size.height()) / 2; const auto rect = QRect(QPoint(left, top), size); p.drawEllipse(rect); icon->paintInCenter(p, rect); }, inner->lifetime()); return result; } [[nodiscard]] object_ptr MakeTitle( QWidget *parent, const style::StealthBoxStyle &st) { return object_ptr>( parent, object_ptr( parent, tr::lng_stealth_mode_title(tr::now), st.box.title), st::storiesStealthTitleMargin); } [[nodiscard]] object_ptr MakeAbout( QWidget *parent, rpl::producer state, const style::StealthBoxStyle &st) { auto text = std::move(state) | rpl::map([](const State &state) { return state.premium ? tr::lng_stealth_mode_about(tr::now) : tr::lng_stealth_mode_unlock_about(tr::now); }); return object_ptr>( parent, object_ptr( parent, std::move(text), st.about), st::storiesStealthAboutMargin); } [[nodiscard]] object_ptr MakeButton( QWidget *parent, rpl::producer state, const style::StealthBoxStyle &st) { auto text = rpl::duplicate(state) | rpl::map([](const State &state) { if (!state.premium) { return tr::lng_stealth_mode_unlock(); } else if (state.mode.cooldownTill <= state.now) { return state.hasCallback ? tr::lng_stealth_mode_enable_and_open() : tr::lng_stealth_mode_enable(); } return rpl::single( rpl::empty ) | rpl::then( base::timer_each(250) ) | rpl::map([=] { const auto now = base::unixtime::now(); const auto left = std::max(state.mode.cooldownTill - now, 1); return tr::lng_stealth_mode_cooldown_in( tr::now, lt_left, TimeLeftText(left)); }) | rpl::type_erased; }) | rpl::flatten_latest(); auto result = object_ptr( parent, rpl::single(QString()), st.box.button); const auto raw = result.data(); const auto label = Ui::CreateChild( raw, std::move(text), st.buttonLabel); label->setAttribute(Qt::WA_TransparentForMouseEvents); label->show(); const auto lock = Ui::CreateChild(raw); lock->setAttribute(Qt::WA_TransparentForMouseEvents); lock->resize(st.lockIcon.size()); lock->paintRequest( ) | rpl::on_next([=, &st] { auto p = QPainter(lock); st.lockIcon.paintInCenter(p, lock->rect()); }, lock->lifetime()); const auto lockLeft = -st.buttonLabel.style.font->height; const auto updateLabelLockGeometry = [=, &st] { const auto outer = raw->width(); const auto added = -st.box.button.width; const auto skip = lock->isHidden() ? 0 : (lockLeft + lock->width()); const auto width = outer - added - skip; const auto top = st.box.button.textTop; label->resizeToWidth(width); label->move(added / 2, top); const auto inner = std::min(label->textMaxWidth(), width); const auto right = (added / 2) + (outer - inner) / 2 + inner; const auto lockTop = (label->height() - lock->height()) / 2; lock->move(right + lockLeft, top + lockTop); }; std::move(state) | rpl::on_next([=](const State &state) { const auto cooldown = state.premium && (state.mode.cooldownTill > state.now); label->setOpacity(cooldown ? kCooldownButtonLabelOpacity : 1.); lock->setVisible(!state.premium); updateLabelLockGeometry(); }, label->lifetime()); raw->widthValue( ) | rpl::on_next(updateLabelLockGeometry, label->lifetime()); return result; } [[nodiscard]] object_ptr StealthModeBox( std::shared_ptr show, Fn onActivated, const style::StealthBoxStyle &st) { return Box([=](not_null box) { struct Data { rpl::variable state; bool requested = false; }; const auto data = box->lifetime().make_state(); data->state = StateValue(&show->session(), onActivated != nullptr); box->setWidth(st::boxWideWidth); box->setStyle(st.box); box->addRow(MakeLogo(box, st)); box->addRow(MakeTitle(box, st), style::al_top); box->addRow(MakeAbout(box, data->state.value(), st), style::al_top); const auto make = [&](const Ui::FeatureListEntry &entry) { return Ui::MakeFeatureListEntry( box, entry, {}, st.featureTitle, st.featureAbout); }; box->addRow(make(FeaturePast(st))); box->addRow( make(FeatureNext(st)), (st::boxRowPadding + QMargins(0, 0, 0, st::storiesStealthBoxBottom))); box->setNoContentMargin(true); box->addTopButton(st.boxClose, [=] { box->closeBox(); }); const auto button = box->addButton( MakeButton(box, data->state.value(), st)); button->resizeToWidth(st::boxWideWidth - st.box.buttonPadding.left() - st.box.buttonPadding.right()); button->setClickedCallback([=] { const auto now = data->state.current(); if (now.mode.enabledTill > now.now) { show->showToast(ToastActivated()); box->closeBox(); } else if (!now.premium) { data->requested = false; if (const auto window = show->resolveWindow()) { ShowPremiumPreviewBox(window, PremiumFeature::Stories); window->window().activate(); } } else if (now.mode.cooldownTill > now.now) { show->showToast(ToastCooldown()); box->closeBox(); } else if (!data->requested) { data->requested = true; show->session().data().stories().activateStealthMode( crl::guard(box, [=] { data->requested = false; })); } }); data->state.value() | rpl::filter([](const State &state) { return state.mode.enabledTill > state.now; }) | rpl::on_next([=] { box->closeBox(); show->showToast(ToastActivated()); if (onActivated) { onActivated(); } }, box->lifetime()); }); } } // namespace void SetupStealthMode( std::shared_ptr show, StealthModeDescriptor descriptor) { const auto onActivated = descriptor.onActivated; const auto st = descriptor.st; const auto now = base::unixtime::now(); const auto mode = show->session().data().stories().stealthMode(); if (const auto left = mode.enabledTill - now; left > 0) { show->showToast(ToastAlready(left)); if (onActivated) { onActivated(); } } else { const auto &style = st ? *st : st::storiesStealthStyle; show->show(StealthModeBox(show, onActivated, style)); } } void AddStealthModeMenu( const Ui::Menu::MenuCallback &add, not_null peer, not_null controller) { if (!peer->session().premiumPossible() || !peer->isUser()) { return; } const auto now = base::unixtime::now(); const auto stealth = peer->owner().stories().stealthMode(); add( tr::lng_stories_view_anonymously(tr::now), [=] { SetupStealthMode( controller->uiShow(), StealthModeDescriptor{ [=] { controller->openPeerStories(peer->id); }, &st::storiesStealthStyleDefault, }); }, ((peer->session().premium() || (stealth.enabledTill > now)) ? &st::menuIconStealth : &st::menuIconStealthLocked)); } QString TimeLeftText(int left) { Expects(left >= 0); const auto hours = left / 3600; const auto minutes = (left % 3600) / 60; const auto seconds = left % 60; const auto zero = QChar('0'); if (hours) { return u"%1:%2:%3"_q .arg(hours) .arg(minutes, 2, 10, zero) .arg(seconds, 2, 10, zero); } else if (minutes) { return u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, zero); } return u"0:%1"_q.arg(left, 2, 10, zero); } } // namespace Media::Stories