diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8fb9e62d1..10b47447d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -997,6 +997,8 @@ PRIVATE media/stories/media_stories_sibling.h media/stories/media_stories_slider.cpp media/stories/media_stories_slider.h + media/stories/media_stories_stealth.cpp + media/stories/media_stories_stealth.h media/stories/media_stories_view.cpp media/stories/media_stories_view.h media/streaming/media_streaming_audio_track.cpp diff --git a/Telegram/Resources/icons/mediaview/download_locked.png b/Telegram/Resources/icons/mediaview/download_locked.png new file mode 100644 index 000000000..206d5893d Binary files /dev/null and b/Telegram/Resources/icons/mediaview/download_locked.png differ diff --git a/Telegram/Resources/icons/mediaview/download_locked@2x.png b/Telegram/Resources/icons/mediaview/download_locked@2x.png new file mode 100644 index 000000000..46bac9086 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/download_locked@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/download_locked@3x.png b/Telegram/Resources/icons/mediaview/download_locked@3x.png new file mode 100644 index 000000000..57752e830 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/download_locked@3x.png differ diff --git a/Telegram/Resources/icons/menu/download_locked.png b/Telegram/Resources/icons/menu/download_locked.png new file mode 100644 index 000000000..824ca3707 Binary files /dev/null and b/Telegram/Resources/icons/menu/download_locked.png differ diff --git a/Telegram/Resources/icons/menu/download_locked@2x.png b/Telegram/Resources/icons/menu/download_locked@2x.png new file mode 100644 index 000000000..1e2dde2ce Binary files /dev/null and b/Telegram/Resources/icons/menu/download_locked@2x.png differ diff --git a/Telegram/Resources/icons/menu/download_locked@3x.png b/Telegram/Resources/icons/menu/download_locked@3x.png new file mode 100644 index 000000000..69d05e38d Binary files /dev/null and b/Telegram/Resources/icons/menu/download_locked@3x.png differ diff --git a/Telegram/Resources/icons/menu/stealth.png b/Telegram/Resources/icons/menu/stealth.png new file mode 100644 index 000000000..af12353a9 Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth.png differ diff --git a/Telegram/Resources/icons/menu/stealth@2x.png b/Telegram/Resources/icons/menu/stealth@2x.png new file mode 100644 index 000000000..50706a61f Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth@2x.png differ diff --git a/Telegram/Resources/icons/menu/stealth@3x.png b/Telegram/Resources/icons/menu/stealth@3x.png new file mode 100644 index 000000000..c659d25b8 Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth@3x.png differ diff --git a/Telegram/Resources/icons/menu/stealth_locked.png b/Telegram/Resources/icons/menu/stealth_locked.png new file mode 100644 index 000000000..dbc0cb5e3 Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth_locked.png differ diff --git a/Telegram/Resources/icons/menu/stealth_locked@2x.png b/Telegram/Resources/icons/menu/stealth_locked@2x.png new file mode 100644 index 000000000..979e23d5c Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth_locked@2x.png differ diff --git a/Telegram/Resources/icons/menu/stealth_locked@3x.png b/Telegram/Resources/icons/menu/stealth_locked@3x.png new file mode 100644 index 000000000..07278760a Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth_locked@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next.png b/Telegram/Resources/icons/stories/next.png similarity index 100% rename from Telegram/Resources/icons/mediaview/stories_next.png rename to Telegram/Resources/icons/stories/next.png diff --git a/Telegram/Resources/icons/mediaview/stories_next@2x.png b/Telegram/Resources/icons/stories/next@2x.png similarity index 100% rename from Telegram/Resources/icons/mediaview/stories_next@2x.png rename to Telegram/Resources/icons/stories/next@2x.png diff --git a/Telegram/Resources/icons/mediaview/stories_next@3x.png b/Telegram/Resources/icons/stories/next@3x.png similarity index 100% rename from Telegram/Resources/icons/mediaview/stories_next@3x.png rename to Telegram/Resources/icons/stories/next@3x.png diff --git a/Telegram/Resources/icons/stories/stealth_25m.png b/Telegram/Resources/icons/stories/stealth_25m.png new file mode 100644 index 000000000..77fb7fdbb Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_25m.png differ diff --git a/Telegram/Resources/icons/stories/stealth_25m@2x.png b/Telegram/Resources/icons/stories/stealth_25m@2x.png new file mode 100644 index 000000000..6ec9a01f7 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_25m@2x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_25m@3x.png b/Telegram/Resources/icons/stories/stealth_25m@3x.png new file mode 100644 index 000000000..e1601cd0e Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_25m@3x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_5m.png b/Telegram/Resources/icons/stories/stealth_5m.png new file mode 100644 index 000000000..51f1cdfc3 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_5m.png differ diff --git a/Telegram/Resources/icons/stories/stealth_5m@2x.png b/Telegram/Resources/icons/stories/stealth_5m@2x.png new file mode 100644 index 000000000..dd0804fea Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_5m@2x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_5m@3x.png b/Telegram/Resources/icons/stories/stealth_5m@3x.png new file mode 100644 index 000000000..c635dec9f Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_5m@3x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_logo.png b/Telegram/Resources/icons/stories/stealth_logo.png new file mode 100644 index 000000000..10a249195 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_logo.png differ diff --git a/Telegram/Resources/icons/stories/stealth_logo@2x.png b/Telegram/Resources/icons/stories/stealth_logo@2x.png new file mode 100644 index 000000000..4300b18f1 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_logo@2x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_logo@3x.png b/Telegram/Resources/icons/stories/stealth_logo@3x.png new file mode 100644 index 000000000..161618d20 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_logo@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 38577fef1..e6dfeeb0a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3877,6 +3877,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_archive_done_many#one" = "{count} story is hidden from your profile."; "lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile."; +"lng_stealth_mode_menu_item" = "Stealth Mode"; +"lng_stealth_mode_title" = "Stealth Mode"; +"lng_stealth_mode_unlock_about" = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them."; +"lng_stealth_mode_about" = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them."; +"lng_stealth_mode_past_title" = "Hide Recent Views"; +"lng_stealth_mode_past_about" = "Hide my views in the last 5 minutes."; +"lng_stealth_mode_next_title" = "Hide Next Views"; +"lng_stealth_mode_next_about" = "Hide my views in the next 25 minutes."; +"lng_stealth_mode_unlock" = "Unlock Stealth Mode"; +"lng_stealth_mode_enable" = "Enable Stealth Mode"; +"lng_stealth_mode_cooldown_in" = "Available in {left}"; +"lng_stealth_mode_cooldown_tip" = "Please wait until the **Stealth Mode** is ready to use again."; +"lng_stealth_mode_enabled_tip_title" = "Stealth Mode On"; +"lng_stealth_mode_enabled_tip" = "The creators of stories you viewed in the last **5 minutes** or will view in the next **25 minutes** won't see you in the viewers' lists."; +"lng_stealth_mode_countdown" = "Stealth Mode active – {left}"; +"lng_stealth_mode_already_title" = "You are in Stealth Mode"; +"lng_stealth_mode_already_about" = "The creators of stories you will view in the next **{left}** won't see you in the viewers' lists."; + "lng_stories_link_invalid" = "This link is broken or has expired."; // Wnd specific diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index c2a1399da..78b286db0 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2528,6 +2528,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->data().stories().apply(update.c_updateReadStories()); } break; + case mtpc_updateStoriesStealthMode: { + const auto &data = update.c_updateStoriesStealthMode(); + _session->data().stories().apply(data.vstealth_mode()); + } break; + } } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 085958ea2..8aa84afd2 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -174,6 +174,14 @@ void Stories::apply(const MTPDupdateReadStories &data) { bumpReadTill(peerFromUser(data.vuser_id()), data.vmax_id().v); } +void Stories::apply(const MTPStoriesStealthMode &stealthMode) { + const auto &data = stealthMode.data(); + _stealthMode = StealthMode{ + .enabledTill = data.vactive_until_date().value_or_empty(), + .cooldownTill = data.vcooldown_until_date().value_or_empty(), + }; +} + void Stories::apply(not_null peer, const MTPUserStories *data) { if (!data) { applyDeletedFromSources(peer->id, StorySourcesList::NotHidden); @@ -536,6 +544,10 @@ void Stories::loadMore(StorySourcesList list) { }, [](const MTPDstories_allStoriesNotModified &) { }); + result.match([&](const auto &data) { + apply(data.vstealth_mode()); + }); + preloadListsMore(); }).fail([=] { _loadMoreRequestId[index] = 0; @@ -719,6 +731,7 @@ void Stories::applyDeleted(FullStoryId id) { } } if (_preloading && _preloading->id() == id) { + _preloading = nullptr; preloadFinished(id); } _owner->refreshStoryItemViews(id); @@ -836,6 +849,26 @@ std::shared_ptr Stories::lookupItem(not_null story) { return j->second.lock(); } +StealthMode Stories::stealthMode() const { + return _stealthMode.current(); +} + +rpl::producer Stories::stealthModeValue() const { + return _stealthMode.value(); +} + +void Stories::activateStealthMode(Fn done) { + const auto api = &session().api(); + using Flag = MTPstories_ActivateStealthMode::Flag; + api->request(MTPstories_ActivateStealthMode( + MTP_flags(Flag::f_past | Flag::f_future) + )).done([=](const MTPBool &result) { + if (done) done(); + }).fail([=] { + if (done) done(); + }).send(); +} + std::shared_ptr Stories::resolveItem(not_null story) { auto &items = _items[story->peer()->id]; auto i = items.find(story->id()); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 48ea79688..545acdfe0 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -116,6 +116,14 @@ struct StoriesContext { friend inline bool operator==(StoriesContext, StoriesContext) = default; }; +struct StealthMode { + TimeId enabledTill = 0; + TimeId cooldownTill = 0; + + friend inline auto operator<=>(StealthMode, StealthMode) = default; + friend inline bool operator==(StealthMode, StealthMode) = default; +}; + inline constexpr auto kStorySourcesListCount = 2; class Stories final : public base::has_weak_ptr { @@ -139,6 +147,7 @@ public: void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); void apply(const MTPDupdateReadStories &data); + void apply(const MTPStoriesStealthMode &stealthMode); void apply(not_null peer, const MTPUserStories *data); Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story); void loadAround(FullStoryId id, StoriesContext context); @@ -227,6 +236,10 @@ public: [[nodiscard]] std::shared_ptr lookupItem( not_null story); + [[nodiscard]] StealthMode stealthMode() const; + [[nodiscard]] rpl::producer stealthModeValue() const; + void activateStealthMode(Fn done = nullptr); + private: struct Saved { StoriesIds ids; @@ -375,6 +388,8 @@ private: base::Timer _pollingTimer; base::Timer _pollingViewsTimer; + rpl::variable _stealthMode; + }; } // namespace Data diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 237401606..2ee9496dd 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_recent_views.h" #include "media/stories/media_stories_reply.h" #include "media/stories/media_stories_share.h" +#include "media/stories/media_stories_stealth.h" #include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" #include "ui/boxes/confirm_box.h" @@ -1522,6 +1523,17 @@ void Controller::tryProcessKeyInput(not_null e) { _replyArea->tryProcessKeyInput(e); } +bool Controller::allowStealthMode() const { + const auto story = this->story(); + return story + && !story->peer()->isSelf() + && story->peer()->session().premiumPossible(); +} + +void Controller::setupStealthMode() { + SetupStealthMode(uiShow()); +} + rpl::lifetime &Controller::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 35d82c9b0..a9703c423 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -167,6 +167,9 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); + [[nodiscard]] bool allowStealthMode() const; + void setupStealthMode(); + [[nodiscard]] rpl::lifetime &lifetime(); private: diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp new file mode 100644 index 000000000..b5d441ee1 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -0,0 +1,407 @@ +/* +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 "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/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.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_layers.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; +}; + +struct Feature { + const style::icon &icon; + QString title; + TextWithEntities about; +}; + +[[nodiscard]] QString LeftText(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); +} + +[[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{ LeftText(left) }, + Ui::Text::RichLangValue), + .st = &st::storiesStealthToast, + .duration = kAlreadyToastDuration, + .adaptive = true, + }; +} + +[[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, + Ui::Text::RichLangValue), + .st = &st::storiesStealthToast, + .duration = kAlreadyToastDuration, + .adaptive = true, + }; +} + +[[nodiscard]] Ui::Toast::Config ToastCooldown() { + return { + .text = tr::lng_stealth_mode_cooldown_tip( + tr::now, + Ui::Text::RichLangValue), + .st = &st::storiesStealthToast, + .duration = kAlreadyToastDuration, + .adaptive = true, + }; +} + +[[nodiscard]] rpl::producer StateValue( + not_null session) { + 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 }); + } + if (left <= 0) { + consumer.put_done(); + } + }; + info->timer.setCallback(check); + check(); + return lifetime; + }); + }) | rpl::flatten_latest(); +} + +[[nodiscard]] Feature FeaturePast() { + return { + .icon = st::storiesStealthFeaturePastIcon, + .title = tr::lng_stealth_mode_past_title(tr::now), + .about = tr::lng_stealth_mode_past_about(tr::now), + }; +} + +[[nodiscard]] Feature FeatureNext() { + return { + .icon = st::storiesStealthFeatureNextIcon, + .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 auto add = st::storiesStealthLogoAdd; + const auto icon = &st::storiesStealthLogoIcon; + 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::start_with_next([=] { + auto p = QPainter(inner); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::storiesComposeBlue); + 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) { + return object_ptr>( + parent, + object_ptr( + parent, + tr::lng_stealth_mode_title(tr::now), + st::storiesStealthBox.title), + st::storiesStealthTitleMargin); +} + +[[nodiscard]] object_ptr MakeAbout( + QWidget *parent, + rpl::producer state) { + 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::storiesStealthAbout), + st::storiesStealthAboutMargin); +} + +[[nodiscard]] object_ptr MakeFeature( + QWidget *parent, + Feature feature) { + auto result = object_ptr>( + parent, + object_ptr(parent), + st::storiesStealthFeatureMargin); + const auto widget = result->entity(); + const auto icon = Ui::CreateChild( + widget, + feature.icon, + st::storiesStealthFeatureIconPosition); + const auto title = Ui::CreateChild( + widget, + feature.title, + st::storiesStealthFeatureTitle); + const auto about = Ui::CreateChild( + widget, + rpl::single(feature.about), + st::storiesStealthFeatureAbout); + icon->show(); + title->show(); + about->show(); + widget->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto left = st::storiesStealthFeatureLabelLeft; + const auto available = width - left; + title->resizeToWidth(available); + about->resizeToWidth(available); + auto top = 0; + title->move(left, top); + top += title->height() + st::storiesStealthFeatureSkip; + about->move(left, top); + top += about->height(); + widget->resize(width, top); + }, widget->lifetime()); + return result; +} + +[[nodiscard]] object_ptr MakeButton( + QWidget *parent, + rpl::producer state) { + 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 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, + LeftText(left)); + }) | rpl::type_erased(); + }) | rpl::flatten_latest(); + + auto result = object_ptr( + parent, + rpl::single(QString()), + st::storiesStealthBox.button); + const auto raw = result.data(); + + const auto label = Ui::CreateChild( + raw, + std::move(text), + st::storiesStealthButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->show(); + + const auto lock = Ui::CreateChild(raw); + lock->setAttribute(Qt::WA_TransparentForMouseEvents); + lock->resize(st::storiesStealthLockIcon.size()); + lock->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(lock); + st::storiesStealthLockIcon.paintInCenter(p, lock->rect()); + }, lock->lifetime()); + + const auto lockLeft = -st::storiesStealthButtonLabel.style.font->height; + const auto updateLabelLockGeometry = [=] { + const auto outer = raw->width(); + const auto added = -st::storiesStealthBox.button.width; + const auto skip = lock->isHidden() ? 0 : (lockLeft + lock->width()); + const auto width = outer - added - skip; + const auto top = st::storiesStealthBox.button.textTop; + label->resizeToWidth(width); + label->move(added / 2, top); + const auto inner = std::min(label->textMaxWidth(), width); + const auto right = (added / 2) + (width - inner) / 2 + inner; + const auto lockTop = (label->height() - lock->height()) / 2; + lock->move(right + lockLeft, top + lockTop); + }; + + std::move(state) | rpl::start_with_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::start_with_next(updateLabelLockGeometry, label->lifetime()); + + return result; +} + +[[nodiscard]] object_ptr StealthModeBox( + std::shared_ptr show) { + 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()); + box->setWidth(st::boxWideWidth); + box->setStyle(st::storiesStealthBox); + box->addRow(MakeLogo(box)); + box->addRow(MakeTitle(box)); + box->addRow(MakeAbout(box, data->state.value())); + box->addRow(MakeFeature(box, FeaturePast())); + box->addRow( + MakeFeature(box, FeatureNext()), + (st::boxRowPadding + + QMargins(0, 0, 0, st::storiesStealthBoxBottom))); + box->setNoContentMargin(true); + box->addTopButton(st::storiesStealthBoxClose, [=] { + box->closeBox(); + }); + const auto button = box->addButton( + MakeButton(box, data->state.value())); + button->resizeToWidth(st::boxWideWidth + - st::storiesStealthBox.buttonPadding.left() + - st::storiesStealthBox.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; + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (const auto window = show->resolveWindow(usage)) { + Settings::ShowPremium( + window, + u"stories_stealth_mode"_q); + 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::start_with_next([=] { + box->closeBox(); + show->showToast(ToastActivated()); + }, box->lifetime()); + }); +} + +} // namespace + +void SetupStealthMode(std::shared_ptr show) { + 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)); + } else { + show->show(StealthModeBox(show)); + } +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.h b/Telegram/SourceFiles/media/stories/media_stories_stealth.h new file mode 100644 index 000000000..d4ccae4b6 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.h @@ -0,0 +1,18 @@ +/* +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 +*/ +#pragma once + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Media::Stories { + +void SetupStealthMode(std::shared_ptr show); + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index dfbab352b..c3cdbd01b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -111,6 +111,14 @@ void View::tryProcessKeyInput(not_null e) { _controller->tryProcessKeyInput(e); } +bool View::allowStealthMode() const { + return _controller->allowStealthMode(); +} + +void View::setupStealthMode() { + _controller->setupStealthMode(); +} + SiblingView View::sibling(SiblingType type) const { return _controller->sibling(type); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 4d6cdac1e..b3dee4aa5 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -91,6 +91,9 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); + [[nodiscard]] bool allowStealthMode() const; + void setupStealthMode(); + [[nodiscard]] rpl::lifetime &lifetime(); private: diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index adf770688..e291abe45 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -413,8 +413,8 @@ storiesSiblingWidthMin: 200px; // Try making sibling not less than this. storiesMaxNameFontSize: 17px; storiesRadius: 8px; storiesControlSize: 64px; -storiesLeft: icon {{ "mediaview/stories_next-flip_horizontal", mediaviewControlFg }}; -storiesRight: icon {{ "mediaview/stories_next", mediaviewControlFg }}; +storiesLeft: icon {{ "stories/next-flip_horizontal", mediaviewControlFg }}; +storiesRight: icon {{ "stories/next", mediaviewControlFg }}; storiesSliderWidth: 2px; storiesSliderMargin: margins(8px, 7px, 8px, 6px); storiesSliderSkip: 4px; @@ -911,3 +911,73 @@ storiesInfoTooltipMaxWidth: 360px; storiesCaptionPullThreshold: 50px; storiesShowMorePadding: margins(6px, 4px, 6px, 4px); storiesShowMoreFont: semiboldFont; + +storiesStealthLogoIcon: icon{{ "stories/stealth_logo", storiesComposeWhiteText }}; +storiesStealthLogoAdd: 12px; +storiesStealthLogoMargin: margins(0px, 28px, 0px, 7px); +storiesStealthBox: Box(defaultBox) { + buttonPadding: margins(10px, 10px, 10px, 10px); + buttonHeight: 42px; + button: RoundButton(defaultBoxButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + + textFg: storiesComposeWhiteText; + textFgOver: storiesComposeWhiteText; + numbersTextFg: storiesComposeWhiteText; + numbersTextFgOver: storiesComposeWhiteText; + textBg: storiesComposeBlue; + textBgOver: storiesComposeBlue; + + ripple: universalRippleAnimation; + } + margin: margins(0px, 56px, 0px, 10px); + bg: groupCallMembersBg; + title: FlatLabel(boxTitle) { + textFg: groupCallMembersFg; + align: align(top); + } + titleAdditionalFg: groupCallMemberNotJoinedStatus; +} +storiesStealthButtonLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: storiesComposeWhiteText; + align: align(top); + minWidth: 20px; + maxHeight: 20px; +} +storiesStealthLockIcon: icon {{ "dialogs/dialogs_lock_on", storiesComposeWhiteText }}; +storiesStealthTitleMargin: margins(0px, 10px, 0px, 0px); +storiesStealthBoxClose: IconButton(defaultIconButton) { + width: boxTitleHeight; + height: boxTitleHeight; + + icon: icon {{ "box_button_close", storiesComposeGrayIcon }}; + iconOver: icon {{ "box_button_close", storiesComposeGrayIcon }}; + + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 40px; + ripple: storiesComposeRippleLight; +} +storiesStealthAbout: FlatLabel(defaultFlatLabel) { + textFg: storiesComposeGrayText; + align: align(top); + minWidth: 20px; +} +storiesStealthAboutMargin: margins(0px, 5px, 0px, 15px); +storiesStealthFeatureTitle: storiesHeaderName; +storiesStealthFeatureAbout: FlatLabel(defaultFlatLabel) { + textFg: storiesComposeGrayText; + minWidth: 20px; +} +storiesStealthFeaturePastIcon: icon{{ "stories/stealth_5m", storiesComposeBlue }}; +storiesStealthFeatureNextIcon: icon{{ "stories/stealth_25m", storiesComposeBlue }}; +storiesStealthFeatureIconPosition: point(3px, 7px); +storiesStealthFeatureMargin: margins(0px, 8px, 0px, 6px); +storiesStealthFeatureLabelLeft: 46px; +storiesStealthFeatureSkip: 2px; +storiesStealthBoxBottom: 11px; +storiesStealthToast: Toast(defaultMultilineToast) { + maxWidth: 340px; +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index fb30c3b4d..ee6b3bd53 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1631,6 +1631,15 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { } }, &st::mediaMenuIconReport); }(); + if (_stories && _stories->allowStealthMode()) { + const auto now = base::unixtime::now(); + const auto stealth = _session->data().stories().stealthMode(); + addAction(tr::lng_stealth_mode_menu_item(tr::now), [=] { + _stories->setupStealthMode(); + }, ((_session->premium() || (stealth.enabledTill > now)) + ? &st::mediaMenuIconStealth + : &st::mediaMenuIconStealthLocked)); + } if (story && story->canReport()) { addAction(tr::lng_profile_report(tr::now), [=] { _stories->reportRequested(); @@ -5249,6 +5258,7 @@ void OverlayWidget::setContext( { story->peer->id, story->id }); if (maybeStory) { _stories->show(*maybeStory, story->within); + _dropdown->raise(); } } else { _message = nullptr; @@ -5289,7 +5299,6 @@ void OverlayWidget::setStoriesPeer(PeerData *peer) { updateControlsGeometry(); }, _stories->lifetime()); _storiesChanged.fire({}); - _dropdown->raise(); } } diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index f1b1a2029..dfe5430b2 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -134,6 +134,8 @@ mediaMenuIconProfile: icon {{ "menu/profile", mediaviewMenuFg }}; mediaMenuIconReport: icon {{ "menu/report", mediaviewMenuFg }}; mediaMenuIconSaveStory: icon {{ "menu/stories_save", mediaviewMenuFg }}; mediaMenuIconArchiveStory: icon {{ "menu/stories_archive", mediaviewMenuFg }}; +mediaMenuIconStealthLocked: icon {{ "menu/stealth_locked", mediaviewMenuFg }}; +mediaMenuIconStealth: icon {{ "menu/stealth", mediaviewMenuFg }}; menuIconDeleteAttention: icon {{ "menu/delete", menuIconAttentionColor }}; menuIconLeaveAttention: icon {{ "menu/leave", menuIconAttentionColor }};