From 01b50a84605fa27f2a6d885d1d8c37535c1054ac Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 23 Aug 2022 21:07:00 +0300 Subject: [PATCH] Added initial implementation of TopBar in Premium Settings for user. --- Telegram/Resources/langs/lang.strings | 8 + .../info/settings/info_settings_widget.cpp | 6 +- Telegram/SourceFiles/settings/settings.style | 37 +- .../SourceFiles/settings/settings_premium.cpp | 488 +++++++++++++++++- 4 files changed, 509 insertions(+), 30 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cdcc32295..e49a14109 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1702,6 +1702,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_unlock_stickers" = "Unlock Premium Stickers"; "lng_premium_unlock_emoji" = "Unlock Animated Emoji"; +"lng_premium_emoji_status_title" = "{user} set this emoji from {link} as their current status."; +"lng_premium_emoji_status_about" = "Emoji status is a premium feature. Other features included in **Telegram Premium**:"; +"lng_premium_emoji_status_button" = "Unlock Emoji Status"; + +"lng_premium_summary_user_title" = "{user} is a subscriber of Telegram Premium."; +"lng_premium_summary_user_about" = "Owners of Telegram Premium accounts have exclusive access to multiple additional features."; +"lng_premium_summary_user_button" = "Learn More"; + "lng_premium_summary_title" = "Telegram Premium"; "lng_premium_summary_top_about" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**."; "lng_premium_summary_title_subscribed" = "You are all set!"; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index ca294ceb5..58e5e65f1 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -142,8 +142,10 @@ Widget::Widget( - _pinnedToTop->minimumHeight(); }; - _inner->heightValue( - ) | rpl::start_with_next([=](int h) { + rpl::combine( + _pinnedToTop->heightValue(), + _inner->heightValue() + ) | rpl::start_with_next([=](int, int h) { _flexibleScroll.contentHeightValue.fire(h + heightDiff()); }, _pinnedToTop->lifetime()); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index c9c11a495..3fc75a735 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -428,8 +428,8 @@ notifyPreviewBottomSkip: 9px; settingsPremiumButtonPadding: margins(11px, 11px, 11px, 3px); settingsPremiumTopBarBackIcon: icon {{ "info/info_back", premiumButtonFg }}; settingsPremiumTopBarBackIconOver: icon {{ "info/info_back", premiumButtonFg }}; -settingsPremiumStarSize: size(77px, 73px); -settingsPremiumStarTopSkip: 42px; +settingsPremiumStarSize: size(84px, 81px); +settingsPremiumStarTopSkip: 37px; settingsPremiumTopBarBack: IconButton(infoTopBarBack) { icon: settingsPremiumTopBarBackIcon; iconOver: settingsPremiumTopBarBackIconOver; @@ -469,13 +469,14 @@ settingsPremiumPreviewAboutPadding: margins(24px, 0px, 24px, 11px); settingsPremiumPreviewLinePadding: margins(18px, 0px, 18px, 8px); settingsPremiumTitlePadding: margins(0px, 20px, 0px, 16px); +settingsPremiumAboutTextStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px underline); + linkFontOver: font(12px underline); + lineHeight: 18px; +} settingsPremiumAbout: FlatLabel(defaultFlatLabel) { - style: TextStyle(defaultTextStyle) { - font: font(12px); - linkFont: font(12px underline); - linkFontOver: font(12px underline); - lineHeight: 18px; - } + style: settingsPremiumAboutTextStyle; palette: TextPalette(defaultTextPalette) { linkFg: premiumButtonFg; } @@ -485,5 +486,25 @@ settingsPremiumAbout: FlatLabel(defaultFlatLabel) { } settingsPremiumStatusPadding: margins(22px, 8px, 22px, 2px); +settingsPremiumUserHeight: 223px; +settingsPremiumUserTitlePadding: margins(0px, 16px, 0px, 6px); +settingsPremiumUserTitle: FlatLabel(boxTitle) { + style: TextStyle(defaultTextStyle) { + font: boxTitleFont; + linkFont: boxTitleFont; + linkFontOver: font(16px semibold underline); + lineHeight: 14px; + } + minWidth: 300px; + maxHeight: 0px; + align: align(top); +} +settingsPremiumUserAbout: FlatLabel(boxDividerLabel) { + style: settingsPremiumAboutTextStyle; + minWidth: 315px; + maxHeight: 0px; + align: align(top); +} + settingsPremiumLock: icon{{ "emoji/premium_lock", windowActiveTextFg, point(0px, 1px) }}; settingsPremiumLockSkip: 3px; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 7702281e6..aa32c808d 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -7,23 +7,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_premium.h" +#include "boxes/premium_preview_box.h" +#include "boxes/sticker_set_box.h" +#include "chat_helpers/stickers_lottie.h" // LottiePlayerFromDocument. #include "core/application.h" #include "core/click_handler_types.h" +#include "core/ui_integration.h" // MarkedTextContext. +#include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_peer_values.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" // SerializeCustomEmojiId. +#include "data/stickers/data_stickers.h" +#include "history/view/media/history_view_sticker.h" // EmojiSize. #include "info/info_wrap_widget.h" // Info::Wrap. +#include "info/profile/info_profile_values.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" -#include "boxes/premium_preview_box.h" +#include "lottie/lottie_single_player.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" #include "settings/settings_common.h" #include "settings/settings_premium.h" #include "ui/abstract_button.h" #include "ui/basic_click_handlers.h" #include "ui/effects/gradient.h" #include "ui/effects/premium_graphics.h" -#include "ui/effects/premium_stars.h" -#include "ui/text/text_utilities.h" -#include "ui/text/format_values.h" +#include "ui/effects/premium_stars_colored.h" #include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/gradient_round_button.h" #include "ui/widgets/labels.h" #include "ui/wrap/fade_wrap.h" @@ -31,10 +46,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_controller.h" -#include "data/data_session.h" -#include "main/main_session.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "window/window_session_controller.h" #include "base/unixtime.h" #include "apiwrap.h" @@ -54,6 +65,10 @@ using SectionCustomTopBarData = Info::Settings::SectionCustomTopBarData; constexpr auto kBodyAnimationPart = 0.90; constexpr auto kTitleAdditionalScale = 0.15; +[[nodiscard]] QString Svg() { + return u":/gui/icons/settings/star.svg"_q; +} + namespace Ref { namespace Gift { @@ -352,6 +367,395 @@ QRectF TopBarAbstract::starRect( starSize); }; +class EmojiStatusTopBar final { +public: + EmojiStatusTopBar( + not_null document, + Fn callback, + QSizeF size); + + void setCenter(QPointF position); + void paint(QPainter &p); + +private: + QPixmap paintedPixmap(const QSize &size) const; + + QRectF _rect; + std::shared_ptr _media; + std::unique_ptr _lottie; + rpl::lifetime _lifetime; + +}; + +EmojiStatusTopBar::EmojiStatusTopBar( + not_null document, + Fn callback, + QSizeF size) +: _rect(QPointF(), size) { + const auto sticker = document->sticker(); + Assert(sticker != nullptr); + _media = document->createMediaView(); + _media->checkStickerLarge(); + _media->goodThumbnailWanted(); + + rpl::single() | rpl::then( + document->owner().session().downloaderTaskFinished() + ) | rpl::start_with_next([=] { + if (!_media->loaded()) { + return; + } + _lifetime.destroy(); + if (sticker->isLottie()) { + _lottie = ChatHelpers::LottiePlayerFromDocument( + _media.get(), + ChatHelpers::StickerLottieSize::EmojiInteractionReserved7, // + size.toSize(), + Lottie::Quality::High); + + _lifetime = _lottie->updates( + ) | rpl::start_with_next([=](Lottie::Update update) { + v::match(update.data, [&](const Lottie::Information &) { + callback(_rect.toRect()); + }, [&](const Lottie::DisplayFrameRequest &) { + callback(_rect.toRect()); + }); + }); + } + }, _lifetime); +} + +void EmojiStatusTopBar::setCenter(QPointF position) { + const auto size = _rect.size(); + const auto shift = QPointF(size.width() / 2., size.height() / 2.); + _rect = QRectF(QPointF(position - shift), QPointF(position + shift)); +} + +QPixmap EmojiStatusTopBar::paintedPixmap(const QSize &size) const { + const auto good = _media->goodThumbnail(); + if (const auto image = _media->getStickerLarge()) { + return image->pix(size); + } else if (good) { + return good->pix(size); + } else if (const auto thumbnail = _media->thumbnail()) { + return thumbnail->pix(size, { .options = Images::Option::Blur }); + } + return QPixmap(); +} + +void EmojiStatusTopBar::paint(QPainter &p) { + if (_lottie) { + if (_lottie->ready()) { + const auto info = _lottie->frameInfo({ + .box = (_rect.size() * style::DevicePixelRatio()).toSize(), + }); + + p.drawImage(_rect, info.image); + _lottie->markFrameShown(); + } + } else if (_media) { + p.drawPixmap(_rect, paintedPixmap(_rect.size().toSize()), _rect); + } +} + +class TopBarUser final : public TopBarAbstract { +public: + TopBarUser( + not_null parent, + not_null controller, + not_null peer); + + void setPaused(bool paused) override; + void setTextPosition(int x, int y) override; + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + +private: + void updateTitle( + DocumentData *document, + TextWithEntities name, + not_null controller) const; + void updateAbout(DocumentData *document) const; + + object_ptr _content; + object_ptr _title; + object_ptr _about; + Ui::Premium::ColoredMiniStars _ministars; + + struct { + object_ptr widget; + Ui::Text::String text; + Ui::Animations::Simple animation; + bool shown = false; + QPoint position; + } _smallTop; + + std::unique_ptr _emojiStatus; + QImage _imageStar; + + QRectF _ministarsRect; + QRectF _starRect; + +}; + +TopBarUser::TopBarUser( + not_null parent, + not_null controller, + not_null peer) +: TopBarAbstract(parent) +, _content(this) +, _title(_content, st::settingsPremiumUserTitle) +, _about(_content, st::settingsPremiumUserAbout) +, _ministars(_content) +, _smallTop({ + .widget = object_ptr(this), + .text = Ui::Text::String( + st::boxTitle.style, + tr::lng_premium_summary_title(tr::now)), +}) { + _starRect = TopBarAbstract::starRect(1., 1.); + + auto documentValue = Info::Profile::EmojiStatusIdValue( + peer + ) | rpl::map([=](DocumentId id) -> DocumentData* { + const auto document = id + ? controller->session().data().document(id).get() + : nullptr; + return (document && document->sticker()) ? document : nullptr; + }); + + rpl::combine( + std::move(documentValue), + Info::Profile::NameValue(peer) + ) | rpl::start_with_next([=]( + DocumentData *document, + TextWithEntities name) { + if (document) { + _emojiStatus = std::make_unique( + document, + [=](QRect r) { update(std::move(r)); }, + HistoryView::Sticker::EmojiSize()); + _imageStar = QImage(); + } else { + auto svg = QSvgRenderer(Svg()); + + const auto size = _starRect.size().toSize(); + auto frame = QImage( + size * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + frame.setDevicePixelRatio(style::DevicePixelRatio()); + + auto mask = frame; + mask.fill(Qt::transparent); + { + Painter p(&mask); + auto gradient = QLinearGradient( + 0, + size.height(), + size.width(), + 0); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + p.setPen(Qt::NoPen); + p.setBrush(gradient); + p.drawRect(0, 0, size.width(), size.height()); + } + frame.fill(Qt::transparent); + { + Painter q(&frame); + svg.render(&q, QRect(QPoint(), size)); + q.setCompositionMode(QPainter::CompositionMode_SourceIn); + q.drawImage(0, 0, mask); + } + _imageStar = std::move(frame); + + _emojiStatus = nullptr; + } + + updateTitle(document, name, controller); + updateAbout(document); + }, lifetime()); + + rpl::combine( + _title->sizeValue(), + _about->sizeValue(), + _content->sizeValue() + ) | rpl::start_with_next([=]( + const QSize &titleSize, + const QSize &aboutSize, + const QSize &size) { + const auto rect = TopBarAbstract::starRect(1., 1.); + const auto &padding = st::settingsPremiumUserTitlePadding; + _title->moveToLeft( + (size.width() - titleSize.width()) / 2, + rect.top() + rect.height() + padding.top()); + _about->moveToLeft( + (size.width() - aboutSize.width()) / 2, + _title->y() + titleSize.height() + padding.bottom()); + + const auto aboutBottom = _about->y() + _about->height(); + const auto height = (aboutBottom > st::settingsPremiumUserHeight) + ? aboutBottom + padding.bottom() + : st::settingsPremiumUserHeight; + { + const auto was = maximumHeight(); + const auto now = height; + if (was != now) { + setMaximumHeight(now); + if (was == size.height()) { + resize(size.width(), now); + } + } + } + + _content->resize(size.width(), maximumHeight()); + }, lifetime()); + + sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + _content->resize(size.width(), maximumHeight()); + _content->moveToLeft(0, -(_content->height() - size.height())); + + _smallTop.widget->resize(size.width(), minimumHeight()); + const auto shown = (minimumHeight() * 2 > size.height()); + if (_smallTop.shown != shown) { + _smallTop.shown = shown; + _smallTop.animation.start( + [=] { _smallTop.widget->update(); }, + _smallTop.shown ? 0. : 1., + _smallTop.shown ? 1. : 0., + st::infoTopBarDuration); + } + }, lifetime()); + + _smallTop.widget->paintRequest( + ) | rpl::start_with_next([=] { + Painter p(_smallTop.widget); + + p.setOpacity(_smallTop.animation.value(_smallTop.shown ? 1. : 0.)); + paintEdges(p, st::boxBg); + + p.setPen(st::boxTitleFg); + _smallTop.text.drawLeft( + p, + _smallTop.position.x(), + _smallTop.position.y(), + width(), + width()); + }, lifetime()); + + _content->paintRequest( + ) | rpl::start_with_next([=] { + Painter p(_content); + + _ministars.paint(p); + + if (_emojiStatus) { + _emojiStatus->paint(p); + } else if (!_imageStar.isNull()) { + p.drawImage(_starRect.topLeft(), _imageStar); + } + }, lifetime()); + +} + +void TopBarUser::updateTitle( + DocumentData *document, + TextWithEntities name, + not_null controller) const { + if (!document) { + return _title->setMarkedText( + tr::lng_premium_summary_user_title( + tr::now, + lt_user, + std::move(name), + Ui::Text::WithEntities)); + } + const auto stickerInfo = document->sticker(); + if (!stickerInfo) { + return; + } + const auto &sets = document->owner().stickers().sets(); + const auto it = sets.find(stickerInfo->set.id); + if (it == sets.cend()) { + return; + } + const auto set = it->second.get(); + + const auto text = (set->thumbnailDocumentId ? QChar('0') : QChar()) + + set->title; + const auto linkIndex = 1; + const auto entityEmojiData = Data::SerializeCustomEmojiId( + { set->thumbnailDocumentId }); + const auto entities = EntitiesInText{ + { EntityType::CustomEmoji, 0, 1, entityEmojiData }, + Ui::Text::Link(text, linkIndex).entities.front(), + }; + auto title = tr::lng_premium_emoji_status_title( + tr::now, + lt_user, + std::move(name), + lt_link, + { .text = text, .entities = entities, }, + Ui::Text::WithEntities); + const auto context = Core::MarkedTextContext{ + .session = &controller->session(), + .customEmojiRepaint = [=] { _title->update(); }, + }; + _title->setMarkedText(std::move(title), context); + auto link = std::make_shared([=, + stickerSetIdentifier = stickerInfo->set] { + controller->show( + Box( + controller, + stickerSetIdentifier, + Data::StickersType::Emoji), + Ui::LayerOption::KeepOther); + }); + _title->setLink(linkIndex, std::move(link)); +} + +void TopBarUser::updateAbout(DocumentData *document) const { + _about->setMarkedText((document + ? tr::lng_premium_emoji_status_about + : tr::lng_premium_summary_user_about)( + tr::now, + Ui::Text::RichLangValue)); +} + +void TopBarUser::setPaused(bool paused) { + _ministars.setPaused(paused); +} + +void TopBarUser::setTextPosition(int x, int y) { + _smallTop.position = { x, y }; +} + +void TopBarUser::paintEvent(QPaintEvent *e) { + Painter p(this); + + TopBarAbstract::paintEdges(p, st::boxBg); +} + +void TopBarUser::resizeEvent(QResizeEvent *e) { + _starRect = TopBarAbstract::starRect(1., 1.); + + const auto &rect = _starRect; + const auto center = rect.center(); + const auto size = QSize( + rect.width() * Ui::Premium::MiniStars::kSizeFactor, + rect.height()); + const auto ministarsRect = QRect( + QPoint(center.x() - size.width(), center.y() - size.height()), + QPoint(center.x() + size.width(), center.y() + size.height())); + _ministars.setPosition(ministarsRect.topLeft()); + _ministars.setSize(ministarsRect.size()); + + if (_emojiStatus) { + _emojiStatus->setCenter(_starRect.center()); + } +} + class TopBar final : public TopBarAbstract { public: TopBar( @@ -399,7 +803,7 @@ TopBar::TopBar( , _titlePadding(st::settingsPremiumTitlePadding) , _about(this, std::move(about), st::settingsPremiumAbout) , _ministars([=](const QRect &r) { update(r); }) -, _star(u":/gui/icons/settings/star.svg"_q) { +, _star(Svg()) { std::move( title ) | rpl::start_with_next([=](QString text) { @@ -841,11 +1245,32 @@ QPointer Premium::createPinnedToTop( tr::lng_premium_summary_top_about(Ui::Text::RichLangValue)); }(); - const auto content = Ui::CreateChild( - parent.get(), - _controller, - std::move(title), - std::move(about)); + const auto emojiStatusData = Ref::EmojiStatus::Parse(_ref); + const auto isEmojiStatus = (!!emojiStatusData); + + auto peerWithPremium = [&]() -> PeerData* { + if (isEmojiStatus) { + auto &data = _controller->session().data(); + if (const auto peer = data.peer(emojiStatusData.peerId)) { + return peer; + } + } + return nullptr; + }(); + + const auto content = [&]() -> TopBarAbstract* { + if (peerWithPremium) { + return Ui::CreateChild( + parent.get(), + _controller, + peerWithPremium); + } + return Ui::CreateChild( + parent.get(), + _controller, + std::move(title), + std::move(about)); + }(); _setPaused = [=](bool paused) { content->setPaused(paused); if (_subscribe) { @@ -858,7 +1283,9 @@ QPointer Premium::createPinnedToTop( content->setRoundEdges(wrap == Info::Wrap::Layer); }, content->lifetime()); - content->setMaximumHeight(st::introQrStepsTop); + content->setMaximumHeight(isEmojiStatus + ? st::settingsPremiumUserHeight + : st::introQrStepsTop); content->setMinimumHeight(st::infoLayerTopBarHeight); content->resize(content->width(), content->maximumHeight()); @@ -870,9 +1297,11 @@ QPointer Premium::createPinnedToTop( content, object_ptr( content, - isLayer - ? st::settingsPremiumLayerTopBarBack - : st::settingsPremiumTopBarBack), + isEmojiStatus + ? (isLayer ? st::infoTopBarBack : st::infoLayerTopBarBack) + : (isLayer + ? st::settingsPremiumLayerTopBarBack + : st::settingsPremiumTopBarBack)), st::infoTopBarScale); _back->setDuration(0); _back->toggleOn(isLayer @@ -894,7 +1323,9 @@ QPointer Premium::createPinnedToTop( } else { _close = base::make_unique_q( content, - st::settingsPremiumTopBarClose); + isEmojiStatus + ? st::infoTopBarClose + : st::settingsPremiumTopBarClose); _close->addClickHandler([=] { _controller->parentController()->hideLayer(); _controller->parentController()->hideSpecialLayer(); @@ -921,10 +1352,27 @@ QPointer Premium::createPinnedToBottom( return nullptr; } + auto buttonText = [&]() -> std::optional> { + if (const auto emojiData = Ref::EmojiStatus::Parse(_ref)) { + auto &data = _controller->session().data(); + if (const auto peer = data.peer(emojiData.peerId)) { + return Info::Profile::EmojiStatusIdValue( + peer + ) | rpl::map([](DocumentId id) { + return id + ? tr::lng_premium_emoji_status_button() + : tr::lng_premium_summary_user_button(); + }) | rpl::flatten_latest(); + } + } + return std::nullopt; + }(); + _subscribe = CreateSubscribeButton({ _controller, content, - [=] { return _ref; } + [ref = _ref] { return ref; }, + std::move(buttonText), }); _showFinished.events(