diff --git a/Telegram/Resources/icons/chat/input_like.png b/Telegram/Resources/icons/chat/input_like.png new file mode 100644 index 000000000..c7ccead7e Binary files /dev/null and b/Telegram/Resources/icons/chat/input_like.png differ diff --git a/Telegram/Resources/icons/chat/input_like@2x.png b/Telegram/Resources/icons/chat/input_like@2x.png new file mode 100644 index 000000000..965ae814b Binary files /dev/null and b/Telegram/Resources/icons/chat/input_like@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_like@3x.png b/Telegram/Resources/icons/chat/input_like@3x.png new file mode 100644 index 000000000..cb2285df9 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_like@3x.png differ diff --git a/Telegram/Resources/icons/chat/input_liked.png b/Telegram/Resources/icons/chat/input_liked.png new file mode 100644 index 000000000..5a267ca1d Binary files /dev/null and b/Telegram/Resources/icons/chat/input_liked.png differ diff --git a/Telegram/Resources/icons/chat/input_liked@2x.png b/Telegram/Resources/icons/chat/input_liked@2x.png new file mode 100644 index 000000000..600b472be Binary files /dev/null and b/Telegram/Resources/icons/chat/input_liked@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_liked@3x.png b/Telegram/Resources/icons/chat/input_liked@3x.png new file mode 100644 index 000000000..5d6c546e6 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_liked@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e6dfeeb0a..70306ad8f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -272,6 +272,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_error_noforwards_channel" = "Sorry, forwarding from this channel is disabled by admins."; "lng_error_nocopy_group" = "Sorry, copying from this group is disabled by admins."; "lng_error_nocopy_channel" = "Sorry, copying from this channel is disabled by admins."; +"lng_error_nocopy_story" = "Sorry, copying of this story is disabled by the author."; "lng_sure_add_admin_invite" = "This user is not a member of this group. Add them to the group and promote them to admin?"; "lng_sure_add_admin_invite_channel" = "This user is not a subscriber of this channel. Add them to the channel and promote them to admin?"; "lng_sure_add_admin_unremove" = "This user is currently restricted or removed. Are you sure you want to promote them?"; @@ -3876,6 +3877,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_archive_done" = "This story is hidden from your profile."; "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_stories_save_promo" = "Subscribe to {link} to download other people's unprotected stories to disk."; "lng_stealth_mode_menu_item" = "Stealth Mode"; "lng_stealth_mode_title" = "Stealth Mode"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 79c85a03d..75fbfbbb0 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -196,6 +196,8 @@ ComposeControls { send: SendButton; attach: IconButton; emoji: EmojiButton; + like: IconButton; + liked: icon; suggestions: EmojiSuggestions; tabbed: EmojiPan; tabbedHeightMin: pixels; diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h index 2f9915679..ba6f43b4e 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace ChatHelpers { struct ComposeFeatures { + bool likes = false; bool sendAs = true; bool ttlInfo = true; bool botCommandSend = true; diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index fbc678581..e8389ad2c 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -276,8 +276,13 @@ bool Story::edited() const { return _edited; } -bool Story::canDownload() const { - return /*!forbidsForward() || */_peer->isSelf(); +bool Story::canDownloadIfPremium() const { + return !forbidsForward() || _peer->isSelf(); +} + +bool Story::canDownloadChecked() const { + return _peer->isSelf() + || (canDownloadIfPremium() && _peer->session().premium()); } bool Story::canShare() const { diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index 8ea17b7b7..70370f176 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -100,7 +100,8 @@ public: [[nodiscard]] bool forbidsForward() const; [[nodiscard]] bool edited() const; - [[nodiscard]] bool canDownload() const; + [[nodiscard]] bool canDownloadIfPremium() const; + [[nodiscard]] bool canDownloadChecked() const; [[nodiscard]] bool canShare() const; [[nodiscard]] bool canDelete() const; [[nodiscard]] bool canReport() const; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 06f295ee8..8af04e830 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -41,6 +41,7 @@ struct SetHistoryArgs { Fn sendActionFactory; rpl::producer slowmodeSecondsLeft; rpl::producer sendDisabledBySlowmode; + rpl::producer liked; rpl::producer> writeRestriction; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 4901c3bf9..6990d5b22 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1069,9 +1069,10 @@ ComposeControls::ComposeControls( , _wrap(std::make_unique(parent)) , _writeRestricted(std::make_unique(parent)) , _send(std::make_shared(_wrap.get(), _st.send)) -, _attachToggle(Ui::CreateChild( - _wrap.get(), - _st.attach)) +, _like(_features.likes + ? Ui::CreateChild(_wrap.get(), _st.like) + : nullptr) +, _attachToggle(Ui::CreateChild(_wrap.get(), _st.attach)) , _tabbedSelectorToggle(Ui::CreateChild( _wrap.get(), _st.emoji)) @@ -1138,6 +1139,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { | rpl::then(std::move(args.slowmodeSecondsLeft)); _sendDisabledBySlowmode = rpl::single(false) | rpl::then(std::move(args.sendDisabledBySlowmode)); + _liked = args.liked ? std::move(args.liked) : rpl::single(false); _writeRestriction = rpl::single(std::optional()) | rpl::then(std::move(args.writeRestriction)); const auto history = *args.history; @@ -1153,6 +1155,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { initWebpageProcess(); initForwardProcess(); updateBotCommandShown(); + updateLikeShown(); updateMessagesTTLShown(); updateControlsGeometry(_wrap->size()); updateControlsVisibility(); @@ -1559,6 +1562,15 @@ void ComposeControls::init() { _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); } + if (_like) { + _like->setClickedCallback([=] { _likeToggled.fire({}); }); + _liked.value( + ) | rpl::start_with_next([=](bool liked) { + const auto icon = liked ? &_st.liked : nullptr; + _like->setIconOverride(icon, icon); + }, _like->lifetime()); + } + _wrap->sizeValue( ) | rpl::start_with_next([=](QSize size) { updateControlsGeometry(size); @@ -1980,7 +1992,7 @@ void ComposeControls::fieldChanged() { if (!_hasSendText.current() && _preview) { _preview->setState(Data::PreviewState::Allowed); } - if (updateBotCommandShown()) { + if (updateBotCommandShown() || updateLikeShown()) { updateControlsVisibility(); updateControlsGeometry(_wrap->size()); } @@ -2521,6 +2533,7 @@ void ComposeControls::updateControlsGeometry(QSize size) { - st::historySendRight - _send->width() - _tabbedSelectorToggle->width() + - (_likeShown ? _like->width() : 0) - (_botCommandShown ? _botCommandStart->width() : 0) - (_silent ? _silent->width() : 0) - (_ttlInfo ? _ttlInfo->width() : 0); @@ -2560,6 +2573,12 @@ void ComposeControls::updateControlsGeometry(QSize size) { right += _send->width(); _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); + if (_like) { + _like->moveToRight(right, buttonsTop); + if (_likeShown) { + right += _like->width(); + } + } if (_botCommandStart) { _botCommandStart->moveToRight(right, buttonsTop); if (_botCommandShown) { @@ -2584,6 +2603,9 @@ void ComposeControls::updateControlsVisibility() { if (_botCommandStart) { _botCommandStart->setVisible(_botCommandShown); } + if (_like) { + _like->setVisible(_likeShown); + } if (_ttlInfo) { _ttlInfo->show(); } @@ -2598,6 +2620,15 @@ void ComposeControls::updateControlsVisibility() { } } +bool ComposeControls::updateLikeShown() { + auto shown = _like && !HasSendText(_field); + if (_likeShown != shown) { + _likeShown = shown; + return true; + } + return false; +} + bool ComposeControls::updateBotCommandShown() { auto shown = false; const auto peer = _history ? _history->peer.get() : nullptr; @@ -3100,6 +3131,10 @@ rpl::producer> ComposeControls::viewportEvents() const { return _voiceRecordBar->lockViewportEvents(); } +rpl::producer<> ComposeControls::likeToggled() const { + return _likeToggled.events(); +} + bool ComposeControls::isRecording() const { return _voiceRecordBar->isRecording(); } @@ -3123,6 +3158,12 @@ rpl::producer ComposeControls::fieldMenuShownValue() const { return _field->menuShownValue(); } +not_null ComposeControls::likeAnimationTarget() const { + Expects(_like != nullptr); + + return _like; +} + bool ComposeControls::preventsClose(Fn &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index de7002012..80ad5e7b2 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -161,6 +161,7 @@ public: [[nodiscard]] rpl::producer inlineResultChosen() const; [[nodiscard]] rpl::producer sendActionUpdates() const; [[nodiscard]] rpl::producer> viewportEvents() const; + [[nodiscard]] rpl::producer<> likeToggled() const; [[nodiscard]] auto scrollKeyEvents() const -> rpl::producer>; [[nodiscard]] auto editLastMessageRequests() const @@ -221,6 +222,7 @@ public: [[nodiscard]] rpl::producer recordingActiveValue() const; [[nodiscard]] rpl::producer hasSendTextValue() const; [[nodiscard]] rpl::producer fieldMenuShownValue() const; + [[nodiscard]] not_null likeAnimationTarget() const; void applyCloudDraft(); void applyDraft( @@ -292,6 +294,7 @@ private: bool showRecordButton() const; void drawRestrictedWrite(QPainter &p, const QString &error); bool updateBotCommandShown(); + bool updateLikeShown(); void cancelInlineBot(); void clearInlineBot(); @@ -344,6 +347,7 @@ private: Fn _sendActionFactory; rpl::variable _slowmodeSecondsLeft; rpl::variable _sendDisabledBySlowmode; + rpl::variable _liked; rpl::variable> _writeRestriction; rpl::variable _hidden; Mode _mode = Mode::Normal; @@ -354,6 +358,7 @@ private: std::optional _backgroundRect; const std::shared_ptr _send; + Ui::IconButton * const _like = nullptr; const not_null _attachToggle; std::unique_ptr _replaceMedia; const not_null _tabbedSelectorToggle; @@ -386,6 +391,7 @@ private: rpl::event_stream> _scrollKeyEvents; rpl::event_stream> _editLastMessageRequests; rpl::event_stream> _attachRequests; + rpl::event_stream<> _likeToggled; rpl::event_stream _replyNextRequests; rpl::event_stream<> _focusRequests; rpl::variable _recording; @@ -407,6 +413,7 @@ private: mtpRequestId _inlineBotResolveRequestId = 0; bool _isInlineBot = false; bool _botCommandShown = false; + bool _likeShown = false; FullMsgId _editingId; std::shared_ptr _photoEditMedia; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 2ee9496dd..32eae7905 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -313,7 +313,7 @@ Controller::Controller(not_null delegate) .type = Ui::MessageSendingAnimationFrom::Type::Emoji, .globalStartGeometry = id.globalGeometry, .frame = id.icon, - }); + }, _wrap.get()); _replyArea->sendReaction(id.id); unfocusReply(); }, _lifetime); @@ -587,6 +587,18 @@ bool Controller::skipCaption() const { return _captionFullView != nullptr; } +bool Controller::liked() const { + return _liked.current(); +} + +rpl::producer Controller::likedValue() const { + return _liked.value(); +} + +void Controller::toggleLiked(bool liked) { + _liked = liked; +} + void Controller::showFullCaption() { if (_captionText.empty()) { return; @@ -892,6 +904,7 @@ bool Controller::changeShown(Data::Story *story) { story, Data::Stories::Polling::Viewer); } + _liked = false; return true; } @@ -1551,7 +1564,8 @@ void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { void Controller::startReactionAnimation( Data::ReactionId id, - Ui::MessageSendingAnimationFrom from) { + Ui::MessageSendingAnimationFrom from, + not_null target) { Expects(shown()); auto args = Ui::ReactionFlyAnimationArgs{ @@ -1568,7 +1582,7 @@ void Controller::startReactionAnimation( Data::CustomEmojiSizeTag::Isolated); const auto layer = _reactionAnimation->layer(); _wrap->paintRequest() | rpl::start_with_next([=] { - if (!_reactionAnimation->paintBadgeFrame(_wrap.get())) { + if (!_reactionAnimation->paintBadgeFrame(target)) { InvokeQueued(layer, [=] { _reactionAnimation = nullptr; _wrap->update(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index a9703c423..115e7e80a 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -123,6 +123,9 @@ public: [[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] TextWithEntities captionText() const; [[nodiscard]] bool skipCaption() const; + [[nodiscard]] bool liked() const; + [[nodiscard]] rpl::producer likedValue() const; + void toggleLiked(bool liked); void showFullCaption(); void captionClosing(); void captionClosed(); @@ -236,7 +239,8 @@ private: void startReactionAnimation( Data::ReactionId id, - Ui::MessageSendingAnimationFrom from); + Ui::MessageSendingAnimationFrom from, + not_null target); const not_null _delegate; @@ -269,6 +273,7 @@ private: Data::StoriesContext _context; std::optional _source; std::optional _list; + rpl::variable _liked; FullStoryId _waitingForId; int _waitingForDelta = 0; int _index = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index d6fbfd7c6..53a5c3435 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -121,6 +121,7 @@ ReplyArea::ReplyArea(not_null controller) .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), .voiceLockFromBottom = true, .features = { + .likes = true, .sendAs = false, .ttlInfo = false, .botCommandSend = false, @@ -620,6 +621,11 @@ void ReplyArea::initActions() { sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); }, _lifetime); + _controls->likeToggled( + ) | rpl::start_with_next([=] { + _controller->toggleLiked(!_controller->liked()); + }, _lifetime); + _controls->setMimeDataHook([=]( not_null data, Ui::InputField::MimeAction action) { @@ -660,6 +666,7 @@ void ReplyArea::show(ReplyAreaData data) { const auto history = user ? user->owner().history(user).get() : nullptr; _controls->setHistory({ .history = history, + .liked = _controller->likedValue(), }); _controls->clear(); const auto hidden = user && user->isSelf(); @@ -718,6 +725,10 @@ void ReplyArea::tryProcessKeyInput(not_null e) { _controls->tryProcessKeyInput(e); } +not_null ReplyArea::likeAnimationTarget() const { + return _controls->likeAnimationTarget(); +} + void ReplyArea::showPremiumToast(not_null emoji) { // #TODO stories } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 9b9662b4a..c3c13abdf 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -70,6 +70,8 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); + [[nodiscard]] not_null likeAnimationTarget() const; + private: class Cant; diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp index 7bf25a1cf..8bacca224 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -353,7 +353,7 @@ struct Feature { if (const auto window = show->resolveWindow(usage)) { Settings::ShowPremium( window, - u"stories_stealth_mode"_q); + u"stories__stealth_mode"_q); window->window().activate(); } } else if (now.mode.cooldownTill > now.now) { diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index e291abe45..5eb311f76 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -109,6 +109,7 @@ mediaviewRight: icon { { "mediaview/next", mediaviewControlFg } }; mediaviewSave: icon {{ "mediaview/download", mediaviewControlFg }}; +mediaviewSaveLocked: icon {{ "mediaview/download_locked", mediaviewControlFg }}; mediaviewShare: icon {{ "mediaview/viewer_share", mediaviewControlFg }}; mediaviewRotate: icon {{ "mediaview/rotate", mediaviewControlFg }}; mediaviewMore: icon {{ "title_menu_dots", mediaviewControlFg }}; @@ -466,6 +467,10 @@ storiesAttach: IconButton(historyAttach) { iconOver: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; ripple: storiesComposeRippleLight; } +storiesLike: IconButton(storiesAttach) { + icon: icon {{ "chat/input_like", storiesComposeGrayIcon }}; + iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }}; +} storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRemoveSet: IconButton(stickerPanRemoveSet) { @@ -676,6 +681,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } attach: storiesAttach; emoji: storiesAttachEmoji; + like: storiesLike; + liked: icon{{ "chat/input_liked", settingsIconBg1 }}; suggestions: EmojiSuggestions(defaultEmojiSuggestions) { dropdown: InnerDropdown(emojiSuggestionsDropdown) { animation: PanelAnimation(defaultPanelAnimation) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 7851ed7f9..570eed0af 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/view/media_view_overlay_opengl.h" +#include "data/data_peer_values.h" // AmPremiumValue. #include "ui/gl/gl_shader.h" #include "ui/painter.h" #include "media/stories/media_stories_view.h" @@ -122,7 +123,16 @@ OverlayWidget::RendererGL::RendererGL(not_null owner) crl::on_main(this, [=] { _owner->_storiesChanged.events( ) | rpl::start_with_next([=] { - invalidateControls(); + if (_owner->_storiesSession) { + Data::AmPremiumValue( + _owner->_storiesSession + ) | rpl::start_with_next([=] { + invalidateControls(); + }, _storiesLifetime); + } else { + _storiesLifetime.destroy(); + invalidateControls(); + } }, _lifetime); }); } @@ -648,8 +658,7 @@ void OverlayWidget::RendererGL::paintControl( QRect inner, float64 innerOpacity, const style::icon &icon) { - const auto stories = (_owner->_stories != nullptr); - const auto meta = ControlMeta(control, stories); + const auto meta = controlMeta(control); Assert(meta.icon == &icon); const auto overAlpha = overOpacity * kOverBackgroundOpacity; @@ -707,18 +716,25 @@ void OverlayWidget::RendererGL::paintControl( FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset); } -auto OverlayWidget::RendererGL::ControlMeta(Over control, bool stories) --> Control { +auto OverlayWidget::RendererGL::controlMeta(Over control) const -> Control { + const auto stories = [&] { + return (_owner->_stories != nullptr); + }; switch (control) { case Over::Left: return { 0, - stories ? &st::storiesLeft : &st::mediaviewLeft + stories() ? &st::storiesLeft : &st::mediaviewLeft }; case Over::Right: return { 1, - stories ? &st::storiesRight : &st::mediaviewRight + stories() ? &st::storiesRight : &st::mediaviewRight + }; + case Over::Save: return { + 2, + (_owner->saveControlLocked() + ? &st::mediaviewSaveLocked + : &st::mediaviewSave) }; - case Over::Save: return { 2, &st::mediaviewSave }; case Over::Share: return { 3, &st::mediaviewShare }; case Over::Rotate: return { 4, &st::mediaviewRotate }; case Over::More: return { 5, &st::mediaviewMore }; @@ -730,14 +746,13 @@ void OverlayWidget::RendererGL::validateControls() { if (!_controlsImage.image().isNull()) { return; } - const auto stories = (_owner->_stories != nullptr); const auto metas = { - ControlMeta(Over::Left, stories), - ControlMeta(Over::Right, stories), - ControlMeta(Over::Save, stories), - ControlMeta(Over::Share, stories), - ControlMeta(Over::Rotate, stories), - ControlMeta(Over::More, stories), + controlMeta(Over::Left), + controlMeta(Over::Right), + controlMeta(Over::Save), + controlMeta(Over::Share), + controlMeta(Over::Rotate), + controlMeta(Over::More), }; auto maxWidth = 0; auto fullHeight = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index a0342518a..f64fb7e58 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -149,7 +149,7 @@ private: Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount]; static constexpr auto kControlsCount = 6; - [[nodiscard]] static Control ControlMeta(Over control, bool stories); + [[nodiscard]] Control controlMeta(Over control) const; // Last one is for the over circle image. std::array _controlsTextures; @@ -158,6 +158,7 @@ private: bool _shadowsForStories = false; bool _blendingEnabled = false; + rpl::lifetime _storiesLifetime; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 0cd3ca35e..119604e1c 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -86,6 +86,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "layout/layout_document_generic_preview.h" #include "platform/platform_overlay_widget.h" +#include "settings/settings_premium.h" #include "storage/file_download.h" #include "storage/storage_account.h" #include "calls/calls_instance.h" @@ -130,6 +131,7 @@ constexpr auto kIdsPreloadAfter = 28; constexpr auto kLeftSiblingTextureIndex = 1; constexpr auto kRightSiblingTextureIndex = 2; constexpr auto kStoriesControlsOpacity = 1.; +constexpr auto kStorySavePromoDuration = 3 * crl::time(1000); class PipDelegate final : public Pip::Delegate { public: @@ -326,16 +328,18 @@ public: return _widget->_body; } bool valid() const override { - return _widget->_storiesSession != nullptr; + return _widget->_session || _widget->_storiesSession; } operator bool() const override { return valid(); } Main::Session &session() const override { - Expects(_widget->_storiesSession != nullptr); + Expects(_widget->_session || _widget->_storiesSession); - return *_widget->_storiesSession; + return _widget->_session + ? *_widget->_session + : *_widget->_storiesSession; } bool paused(ChatHelpers::PauseReason reason) const override { if (_widget->isHidden() @@ -1024,22 +1028,26 @@ QSize OverlayWidget::flipSizeByRotation(QSize size) const { return FlipSizeByRotation(size, _rotation); } -bool OverlayWidget::hasCopyMediaRestriction() const { - const auto story = _stories ? _stories->story() : nullptr; - return (story && !story->canDownload()) - || (_history && !_history->peer->allowsForwarding()) +bool OverlayWidget::hasCopyMediaRestriction(bool skipPremiumCheck) const { + if (const auto story = _stories ? _stories->story() : nullptr) { + return skipPremiumCheck + ? story->canDownloadIfPremium() + : story->canDownloadChecked(); + } + return (_history && !_history->peer->allowsForwarding()) || (_message && _message->forbidsSaving()); } -bool OverlayWidget::showCopyMediaRestriction() { - if (!hasCopyMediaRestriction()) { +bool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) { + if (!hasCopyMediaRestriction(skipPRemiumCheck)) { return false; - } else if (!_history) { - return true; + } else if (_stories) { + uiShow()->showToast(tr::lng_error_nocopy_story(tr::now)); + } else if (_history) { + uiShow()->showToast(_history->peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); } - Ui::Toast::Show(_widget, _history->peer->isBroadcast() - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -1210,8 +1218,8 @@ void OverlayWidget::refreshNavVisibility() { } } -bool OverlayWidget::contentCanBeSaved() const { - if (hasCopyMediaRestriction()) { +bool OverlayWidget::computeSaveButtonVisible() const { + if (hasCopyMediaRestriction(true)) { return false; } else if (_photo) { return _photo->hasVideo() || _photoMedia->loaded(); @@ -1240,6 +1248,26 @@ void OverlayWidget::checkForSaveLoaded() { } } +void OverlayWidget::showPremiumDownloadPromo() { + const auto filter = [=](const auto &...) { + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (const auto window = uiShow()->resolveWindow(usage)) { + const auto ref = u"stories__save_stories_to_gallery"_q; + Settings::ShowPremium(window, ref); + } + return false; + }; + uiShow()->showToast({ + .text = tr::lng_stories_save_promo( + tr::now, + lt_link, + Ui::Text::Bold(tr::lng_send_as_premium_required_link(tr::now)), + Ui::Text::WithEntities), + .duration = kStorySavePromoDuration, + .filter = filter, + }); +} + void OverlayWidget::updateControls() { if (_document && documentBubbleShown()) { _docRect = QRect( @@ -1290,7 +1318,7 @@ void OverlayWidget::updateControls() { const auto overRect = QRect( QPoint(), QSize(st::mediaviewIconOver, st::mediaviewIconOver)); - _saveVisible = contentCanBeSaved(); + _saveVisible = computeSaveButtonVisible(); _shareVisible = story && story->canShare(); _rotateVisible = !_themePreviewShown && !story; const auto navRect = [&](int i) { @@ -1320,6 +1348,7 @@ void OverlayWidget::updateControls() { _saveNav = navRect(index); _saveNavOver = style::centerrect(_saveNav, overRect); _saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave); + Assert(st::mediaviewSave.size() == st::mediaviewSaveLocked.size()); const auto dNow = QDateTime::currentDateTime(); const auto d = [&] { @@ -1471,7 +1500,7 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { ? &st::mediaMenuIconArchiveStory : &st::mediaMenuIconSaveStory); } - if ((!story || story->canDownload()) + if ((!story || story->canDownloadChecked()) && _document && !_document->filepath(true).isEmpty()) { const auto text = Platform::IsMac() @@ -1533,11 +1562,13 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { [=] { deleteMedia(); }, &st::mediaMenuIconDelete); } - if (!hasCopyMediaRestriction()) { + if (!hasCopyMediaRestriction(true)) { addAction( tr::lng_mediaview_save_as(tr::now), [=] { saveAs(); }, - &st::mediaMenuIconDownload); + (saveControlLocked() + ? &st::mediaMenuIconDownloadLocked + : &st::mediaMenuIconDownload)); } if (const auto overviewType = computeOverviewType()) { @@ -2285,7 +2316,11 @@ void OverlayWidget::notifyFileDialogShown(bool shown) { } void OverlayWidget::saveAs() { - if (showCopyMediaRestriction()) { + if (showCopyMediaRestriction(true)) { + return; + } else if (hasCopyMediaRestriction()) { + Assert(_stories != nullptr); + showPremiumDownloadPromo(); return; } QString file; @@ -2421,9 +2456,13 @@ void OverlayWidget::handleDocumentClick() { void OverlayWidget::downloadMedia() { if (!_photo && !_document) { return; - } - if (Core::App().settings().askDownloadPath()) { + } else if (Core::App().settings().askDownloadPath()) { return saveAs(); + } else if (hasCopyMediaRestriction()) { + if (_stories && !hasCopyMediaRestriction(true)) { + showPremiumDownloadPromo(); + } + return; } QString path; @@ -2478,7 +2517,7 @@ void OverlayWidget::downloadMedia() { } } } else { - _saveVisible = contentCanBeSaved(); + _saveVisible = computeSaveButtonVisible(); update(_saveNavOver); } updateOver(_lastMouseMovePos); @@ -2498,7 +2537,7 @@ void OverlayWidget::downloadMedia() { } } else { if (!_photo || !_photoMedia->loaded()) { - _saveVisible = contentCanBeSaved(); + _saveVisible = computeSaveButtonVisible(); update(_saveNavOver); } else { if (!QDir().exists(path)) { @@ -3899,8 +3938,7 @@ void OverlayWidget::initThemePreview() { _themeShare->setClickedCallback([=] { QGuiApplication::clipboard()->setText( session->createInternalLinkFull("addtheme/" + slug)); - Ui::Toast::Show( - _body, + uiShow()->showToast( tr::lng_background_link_copied(tr::now)); }); } else { @@ -4198,6 +4236,10 @@ not_null OverlayWidget::storiesWrap() { } std::shared_ptr OverlayWidget::storiesShow() { + return uiShow(); +} + +std::shared_ptr OverlayWidget::uiShow() { if (!_cachedShow) { _cachedShow = std::make_shared(this); } @@ -4778,6 +4820,13 @@ void OverlayWidget::paintSaveMsgContent( p.setOpacity(1); } +bool OverlayWidget::saveControlLocked() const { + const auto story = _stories ? _stories->story() : nullptr; + return story + && story->canDownloadIfPremium() + && !story->canDownloadChecked(); +} + void OverlayWidget::paintControls( not_null renderer, float64 opacity) { @@ -4811,7 +4860,9 @@ void OverlayWidget::paintControls( _saveVisible, _saveNavOver, _saveNavIcon, - st::mediaviewSave }, + (saveControlLocked() + ? st::mediaviewSaveLocked + : st::mediaviewSave) }, { Over::Share, _shareVisible, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 06d0c0468..864e21084 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -245,6 +245,7 @@ private: void playbackPauseMusic(); void switchToPip(); [[nodiscard]] int topNotchSkip() const; + [[nodiscard]] std::shared_ptr uiShow(); not_null storiesWrap() override; std::shared_ptr storiesShow() override; @@ -310,8 +311,9 @@ private: void handleScreenChanged(QScreen *screen); - bool contentCanBeSaved() const; + [[nodiscard]] bool computeSaveButtonVisible() const; void checkForSaveLoaded(); + void showPremiumDownloadPromo(); Entity entityForUserPhotos(int index) const; Entity entityForSharedMedia(int index) const; @@ -495,8 +497,10 @@ private: void validatePhotoImage(Image *image, bool blurred); void validatePhotoCurrentImage(); - [[nodiscard]] bool hasCopyMediaRestriction() const; - [[nodiscard]] bool showCopyMediaRestriction(); + [[nodiscard]] bool hasCopyMediaRestriction( + bool skipPremiumCheck = false) const; + [[nodiscard]] bool showCopyMediaRestriction( + bool skipPRemiumCheck = false); [[nodiscard]] QSize flipSizeByRotation(QSize size) const; @@ -518,7 +522,8 @@ private: [[nodiscard]] bool contentShown() const; [[nodiscard]] bool opaqueContentShown() const; void clearStreaming(bool savePosition = true); - bool canInitStreaming() const; + [[nodiscard]] bool canInitStreaming() const; + [[nodiscard]] bool saveControlLocked() const; [[nodiscard]] bool topShadowOnTheRight() const; void applyHideWindowWorkaround(); diff --git a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp index 8e55994cc..c655ab678 100644 --- a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp +++ b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.cpp @@ -95,8 +95,7 @@ void EmojiFlyAnimation::repaint() { } } -bool EmojiFlyAnimation::paintBadgeFrame( - not_null widget) { +bool EmojiFlyAnimation::paintBadgeFrame(not_null widget) { _target = widget; return !_fly.finished(); } diff --git a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h index 7bfd08ce2..f5dc8102b 100644 --- a/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h +++ b/Telegram/SourceFiles/ui/effects/emoji_fly_animation.h @@ -25,7 +25,7 @@ public: [[nodiscard]] bool finished() const; void repaint(); - bool paintBadgeFrame(not_null widget); + bool paintBadgeFrame(not_null widget); private: const int _flySize = 0; @@ -33,7 +33,7 @@ private: Ui::RpWidget _layer; QRect _area; bool _areaUpdated = false; - QPointer _target; + QPointer _target; }; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index dfe5430b2..641390f2e 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -126,6 +126,7 @@ mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }}; mediaMenuIconShowInChat: icon {{ "menu/show_in_chat", mediaviewMenuFg }}; mediaMenuIconShowInFolder: icon {{ "menu/show_in_folder", mediaviewMenuFg }}; mediaMenuIconDownload: icon {{ "menu/download", mediaviewMenuFg }}; +mediaMenuIconDownloadLocked: icon {{ "menu/download_locked", mediaviewMenuFg }}; mediaMenuIconCopy: icon {{ "menu/copy", mediaviewMenuFg }}; mediaMenuIconForward: icon {{ "menu/forward", mediaviewMenuFg }}; mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }};