From 1cfad1443777ea295659f64220c9b4859c52bb9a Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 Jan 2024 13:58:26 +0400 Subject: [PATCH] Show correct placeholder in require-premium story reply. --- Telegram/Resources/langs/lang.strings | 7 +- .../chat_helpers/chat_helpers.style | 8 + .../view/controls/compose_controls_common.h | 27 +- .../history_view_compose_controls.cpp | 237 +++++++++++++++--- .../controls/history_view_compose_controls.h | 7 +- .../view/history_view_replies_section.cpp | 6 +- .../view/history_view_scheduled_section.cpp | 6 +- .../media/stories/media_stories_reply.cpp | 18 ++ .../SourceFiles/media/view/media_view.style | 26 ++ .../SourceFiles/settings/settings_premium.cpp | 1 + Telegram/SourceFiles/ui/effects/premium.style | 8 + 11 files changed, 300 insertions(+), 51 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 5b2f2978df..9c6efb5480 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3618,10 +3618,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_non_premium_text" = "Subscribe to **Premium**\n to message {user}."; "lng_send_non_premium_go" = "Go Premium"; "lng_send_non_premium_story" = "Replies restricted"; -"lng_send_non_premium_unlock" = "Unlock"; -"lng_send_non_premium_story_toast" = "You need a **Premium** subscription to reply to **{user}'s** stories."; -"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and **Premium** users."; -"lng_send_non_premium_toast_button" = "View"; +"lng_send_non_premium_unlock" = "unlock"; +"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers."; +"lng_send_non_premium_message_toast_link" = "Telegram Premium"; "lng_exceptions_list_title" = "Exceptions"; "lng_removed_list_title" = "Removed users"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 454c58d0e0..c692b4cb6a 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -208,6 +208,8 @@ ComposeControls { files: ComposeFiles; premium: PremiumLimits; boxField: InputField; + restrictionLabel: FlatLabel; + premiumRequired: ComposePremiumRequired; } ReportBox { @@ -1218,6 +1220,11 @@ defaultComposeFiles: ComposeFiles { nameFg: historyFileNameInFg; statusFg: mediaInFg; } +defaultRestrictionLabel: FlatLabel(defaultFlatLabel) { + minWidth: 12px; + textFg: placeholderFg; + align: align(top); +} defaultComposeControls: ComposeControls { bg: historyComposeAreaBg; radius: 0px; @@ -1234,6 +1241,7 @@ defaultComposeControls: ComposeControls { files: defaultComposeFiles; premium: defaultPremiumLimits; boxField: defaultInputField; + restrictionLabel: defaultRestrictionLabel; } moreChatsBarHeight: 48px; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 8af04e8307..e707523ecc 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -34,6 +34,31 @@ struct SendActionUpdate { bool cancel = false; }; +enum class WriteRestrictionType { + None, + Rights, + PremiumRequired, +}; + +struct WriteRestriction { + using Type = WriteRestrictionType; + + QString text; + QString button; + Type type = Type::None; + + [[nodiscard]] bool empty() const { + return (type == Type::None); + } + explicit operator bool() const { + return !empty(); + } + + friend inline bool operator==( + const WriteRestriction &a, + const WriteRestriction &b) = default; +}; + struct SetHistoryArgs { required history; MsgId topicRootId = 0; @@ -42,7 +67,7 @@ struct SetHistoryArgs { rpl::producer slowmodeSecondsLeft; rpl::producer sendDisabledBySlowmode; rpl::producer liked; - rpl::producer> writeRestriction; + rpl::producer writeRestriction; }; struct ReplyNextRequest { 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 e8e81511e2..ac7735953e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -62,7 +62,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/session/send_as_peers.h" #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" +#include "settings/settings_premium.h" #include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" #include "ui/ui_utility.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/dropdown_menu.h" @@ -800,7 +802,6 @@ ComposeControls::ComposeControls( : not_null(_ownedSelector.get())) , _mode(descriptor.mode) , _wrap(std::make_unique(parent)) -, _writeRestricted(std::make_unique(parent)) , _send(std::make_shared(_wrap.get(), _st.send)) , _like(_features.likes ? Ui::CreateChild(_wrap.get(), _st.like) @@ -878,7 +879,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { _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()) + _writeRestriction = rpl::single(Controls::WriteRestriction()) | rpl::then(std::move(args.writeRestriction)); const auto history = *args.history; if (_history == history) { @@ -892,6 +893,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { registerDraftSource(); _selector->setCurrentPeer(history ? history->peer.get() : nullptr); initWebpageProcess(); + initWriteRestriction(); initForwardProcess(); updateBotCommandShown(); updateLikeShown(); @@ -941,12 +943,16 @@ PeerData *ComposeControls::sendAsPeer() const { void ComposeControls::move(int x, int y) { _wrap->move(x, y); - _writeRestricted->move(x, y); + if (_writeRestricted) { + _writeRestricted->move(x, y); + } } void ComposeControls::resizeToWidth(int width) { _wrap->resizeToWidth(width); - _writeRestricted->resizeToWidth(width); + if (_writeRestricted) { + _writeRestricted->resizeToWidth(width); + } updateHeight(); } @@ -961,12 +967,12 @@ rpl::producer ComposeControls::height() const { return rpl::conditional( _writeRestriction.value() | rpl::map(!_1), _wrap->heightValue(), - _writeRestricted->heightValue()); + rpl::single(_st.attach.height)); } int ComposeControls::heightCurrent() const { return _writeRestriction.current() - ? _writeRestricted->height() + ? _st.attach.height : _wrap->height(); } @@ -1120,7 +1126,9 @@ void ComposeControls::showStarted() { _autocomplete->hideFast(); } _wrap->hide(); - _writeRestricted->hide(); + if (_writeRestricted) { + _writeRestricted->hide(); + } } void ComposeControls::showFinished() { @@ -1319,7 +1327,8 @@ void ComposeControls::init() { _wrap->paintRequest( ) | rpl::start_with_next([=](QRect clip) { - paintBackground(clip); + auto p = QPainter(_wrap.get()); + paintBackground(p, _wrap->rect(), clip); }, _wrap->lifetime()); _header->editMsgIdValue( @@ -1466,18 +1475,6 @@ void ComposeControls::clearListenState() { _voiceRecordBar->clearListenState(); } -void ComposeControls::drawRestrictedWrite(QPainter &p, const QString &error) { - p.fillRect(_writeRestricted->rect(), st::historyReplyBg); - - p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); - p.drawText( - _writeRestricted->rect().marginsRemoved( - QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), - error, - style::al_center); -} - void ComposeControls::initKeyHandler() { _wrap->events( ) | rpl::filter([=](not_null event) { @@ -2191,24 +2188,164 @@ void ComposeControls::inlineBotChanged() { } } +void SetupRestrictionView( + not_null widget, + not_null st, + std::shared_ptr show, + const QString &name, + rpl::producer restriction, + Fn paintBackground) { + struct State { + std::unique_ptr label; + std::unique_ptr button; + std::unique_ptr unlock; + std::unique_ptr icon; + Fn updateGeometries; + }; + const auto state = widget->lifetime().make_state(); + state->updateGeometries = [=] { + if (!state->label) { + return; + } else if (state->button) { + const auto available = widget->width() + - st->like.width + - st::historySendRight + - state->unlock->width() + - st->premiumRequired.buttonSkip + - st->premiumRequired.position.x(); + state->label->resizeToWidth(available); + state->label->moveToLeft( + st->premiumRequired.position.x(), + st->premiumRequired.position.y(), + widget->width()); + const auto left = st->premiumRequired.position.x() + + std::min(available, state->label->textMaxWidth()) + + st->premiumRequired.buttonSkip; + state->unlock->moveToLeft( + left, + st->premiumRequired.buttonTop, + widget->width()); + state->button->setGeometry(QRect( + QPoint(), + QSize(left + state->unlock->width(), widget->height()))); + state->icon->moveToLeft(0, 0, widget->width()); + } else { + const auto left = st::historySendRight; + state->label->resizeToWidth(widget->width() - 2 * left); + state->label->moveToLeft( + left, + (widget->height() - state->label->height()) / 2, + widget->width()); + } + }; + const auto makeLabel = [=]( + const QString &text, + const style::FlatLabel &st) { + auto label = std::make_unique(widget, text, st); + label->show(); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->heightValue( + ) | rpl::start_with_next( + state->updateGeometries, + label->lifetime()); + return label; + }; + const auto makeUnlock = [=](const QString &text, const QString &name) { + using namespace Ui; + auto unlock = std::make_unique( + widget, + rpl::single(text), + st->premiumRequired.button); + unlock->show(); + unlock->setAttribute(Qt::WA_TransparentForMouseEvents); + unlock->setTextTransform(RoundButton::TextTransform::NoTransform); + unlock->setFullRadius(true); + return unlock; + }; + const auto makeIcon = [=] { + auto icon = std::make_unique(widget); + icon->resize(st->premiumRequired.icon.size()); + icon->show(); + icon->paintRequest() | rpl::start_with_next([st, raw = icon.get()] { + auto p = QPainter(raw); + st->premiumRequired.icon.paint(p, {}, raw->width()); + }, icon->lifetime()); + return icon; + }; + std::move( + restriction + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](Controls::WriteRestriction value) { + using Type = Controls::WriteRestriction::Type; + if (value.type == Type::Rights) { + state->icon = nullptr; + state->unlock = nullptr; + state->button = nullptr; + state->label = makeLabel(value.text, st->restrictionLabel); + } else if (value.type == Type::PremiumRequired) { + state->icon = makeIcon(); + state->unlock = makeUnlock(value.button, name); + state->button = std::make_unique(widget); + state->button->setClickedCallback([=] { + ::Settings::ShowPremiumPromoToast( + show, + tr::lng_send_non_premium_message_toast( + tr::now, + lt_user, + TextWithEntities{ name }, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_send_non_premium_message_toast_link( + tr::now))), + Ui::Text::RichLangValue), + u"require_premium"_q); + }); + state->label = makeLabel(value.text, st->premiumRequired.label); + } + }, widget->lifetime()); + + widget->sizeValue( + ) | rpl::start_with_next(state->updateGeometries, widget->lifetime()); + + widget->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(widget); + paintBackground(p, clip); + }, widget->lifetime()); +} + void ComposeControls::initWriteRestriction() { + if (!_history) { + const auto was = base::take(_writeRestricted); + updateWrappingVisibility(); + return; + } + _writeRestricted = std::make_unique(_parent); + _writeRestricted->move(_wrap->pos()); + _writeRestricted->resizeToWidth(_wrap->widthNoMargins()); + _writeRestricted->sizeValue() | rpl::start_with_next([=] { + if (_like && _like->parentWidget() == _writeRestricted.get()) { + updateControlsGeometry(_wrap->size()); + } + }, _writeRestricted->lifetime()); _writeRestricted->resize( _writeRestricted->width(), st::historyUnblock.height); - _writeRestricted->paintRequest( - ) | rpl::start_with_next([=] { - if (const auto error = _writeRestriction.current()) { - auto p = Painter(_writeRestricted.get()); - drawRestrictedWrite(p, *error); - } - }, _wrap->lifetime()); + const auto background = [=](QPainter &p, QRect clip) { + paintBackground(p, _writeRestricted->rect(), clip); + }; + SetupRestrictionView( + _writeRestricted.get(), + &_st, + _show, + _history->peer->shortName(), + _writeRestriction.value(), + background); _writeRestriction.value( - ) | rpl::filter([=] { - return _wrap->isHidden() || _writeRestricted->isHidden(); - }) | rpl::start_with_next([=] { + ) | rpl::start_with_next([=] { updateWrappingVisibility(); - }, _wrap->lifetime()); + }, _writeRestricted->lifetime()); } void ComposeControls::changeFocusedControl() { @@ -2261,9 +2398,26 @@ void ComposeControls::initVoiceRecordBar() { void ComposeControls::updateWrappingVisibility() { const auto hidden = _hidden.current(); - const auto restricted = _writeRestriction.current().has_value(); - _writeRestricted->setVisible(!hidden && restricted); + const auto &restriction = _writeRestriction.current(); + const auto restricted = !restriction.empty() && _writeRestricted; + if (_writeRestricted) { + _writeRestricted->setVisible(!hidden && restricted); + } _wrap->setVisible(!hidden && !restricted); + using namespace Controls; + if (_like) { + const auto hidden = _like->isHidden(); + if (_writeRestricted + && restriction.type == WriteRestrictionType::PremiumRequired) { + _like->setParent(_writeRestricted.get()); + } else { + _like->setParent(_wrap.get()); + } + if (!hidden) { + _like->show(); + updateControlsGeometry(_wrap->size()); + } + } if (!hidden && !restricted) { _wrap->raise(); } @@ -2363,9 +2517,14 @@ void ComposeControls::updateControlsGeometry(QSize size) { _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); if (_like) { - _like->moveToRight(right, buttonsTop); - if (_likeShown) { - right += _like->width(); + using Type = Controls::WriteRestrictionType; + if (_writeRestriction.current().type == Type::PremiumRequired) { + _like->moveToRight(st::historySendRight, 0); + } else { + _like->moveToRight(right, buttonsTop); + if (_likeShown) { + right += _like->width(); + } } } if (_botCommandStart) { @@ -2521,9 +2680,7 @@ void ComposeControls::updateAttachBotsMenu() { }, _attachBotsMenu->lifetime()); } -void ComposeControls::paintBackground(QRect clip) { - Painter p(_wrap.get()); - +void ComposeControls::paintBackground(QPainter &p, QRect full, QRect clip) { if (_backgroundRect) { //p.setCompositionMode(QPainter::CompositionMode_Source); //p.fillRect(clip, Qt::transparent); @@ -2532,7 +2689,7 @@ void ComposeControls::paintBackground(QRect clip) { auto hq = PainterHighQualityEnabler(p); p.setBrush(_st.bg); p.setPen(Qt::NoPen); - p.drawRoundedRect(_wrap->rect(), _st.radius, _st.radius); + p.drawRoundedRect(full, _st.radius, _st.radius); } else { p.fillRect(clip, _st.bg); } 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 a846cd4ee9..795e25760a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -272,7 +272,7 @@ private: void updateControlsGeometry(QSize size); bool updateReplaceMediaButton(); void updateOuterGeometry(QRect rect); - void paintBackground(QRect clip); + void paintBackground(QPainter &p, QRect full, QRect clip); [[nodiscard]] auto computeSendButtonType() const; [[nodiscard]] SendMenu::Type sendMenuType() const; @@ -296,7 +296,6 @@ private: void setTabbedPanel(std::unique_ptr panel); bool showRecordButton() const; - void drawRestrictedWrite(QPainter &p, const QString &error); bool updateBotCommandShown(); bool updateLikeShown(); @@ -353,12 +352,12 @@ private: rpl::variable _slowmodeSecondsLeft; rpl::variable _sendDisabledBySlowmode; rpl::variable _liked; - rpl::variable> _writeRestriction; + rpl::variable _writeRestriction; rpl::variable _hidden; Mode _mode = Mode::Normal; const std::unique_ptr _wrap; - const std::unique_ptr _writeRestricted; + std::unique_ptr _writeRestricted; rpl::event_stream _jumpToItemRequests; std::optional _backgroundRect; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 157a61977c..dca9b26331 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -699,7 +699,7 @@ void RepliesWidget::setupComposeControls() { const auto restriction = Data::RestrictionError( _history->peer, ChatRestriction::SendOther); - return !canSendAnything + auto text = !canSendAnything ? (restriction ? restriction : topicRestriction @@ -708,6 +708,10 @@ void RepliesWidget::setupComposeControls() { : topicRestriction ? std::move(topicRestriction) : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); }); _composeControls->setHistory({ diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index a086d71cb2..befbce50d3 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -198,11 +198,15 @@ void ScheduledWidget::setupComposeControls() { const auto restriction = Data::RestrictionError( _history->peer, ChatRestriction::SendOther); - return !canSendAnything + auto text = !canSendAnything ? (restriction ? restriction : tr::lng_group_not_accessible(tr::now)) : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); }); _composeControls->setHistory({ .history = _history.get(), diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 8a20887851..d06f32dbb3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "data/data_document.h" #include "data/data_message_reaction_id.h" +#include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/view/controls/compose_controls_common.h" @@ -663,6 +664,22 @@ void ReplyArea::show( invalidate_weak_ptrs(&_shownPeerGuard); const auto peer = data.peer; const auto history = peer ? peer->owner().history(peer).get() : nullptr; + const auto user = peer->asUser(); + auto writeRestriction = Data::CanSendAnythingValue( + peer + ) | rpl::map([=](bool can) { + using namespace HistoryView::Controls; + return (can + || !user + || !user->meRequiresPremiumToWrite() + || user->session().premium()) + ? WriteRestriction() + : WriteRestriction{ + .text = tr::lng_send_non_premium_story(tr::now), + .button = tr::lng_send_non_premium_unlock(tr::now), + .type = WriteRestrictionType::PremiumRequired, + }; + }); _controls->setHistory({ .history = history, .liked = std::move( @@ -670,6 +687,7 @@ void ReplyArea::show( ) | rpl::map([](const Data::ReactionId &id) { return !id.empty(); }), + .writeRestriction = std::move(writeRestriction), }); _controls->clear(); const auto hidden = peer diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c857368f3a..1d33b03b85 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -776,6 +776,32 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { menu: storiesPopupMenu; } + restrictionLabel: FlatLabel(defaultFlatLabel) { + minWidth: 12px; + textFg: storiesComposeGrayText; + align: align(top); + } + premiumRequired: ComposePremiumRequired { + label: FlatLabel(defaultFlatLabel) { + minWidth: 12px; + textFg: storiesComposeGrayText; + } + button: RoundButton(defaultActiveButton) { + textFg: storiesComposeWhiteText; + textFgOver: storiesComposeWhiteText; + textBg: groupCallMembersBgRipple; + textBgOver: groupCallMembersBgRipple; + width: -12px; + height: 18px; + textTop: 0px; + font: font(12px); + ripple: storiesComposeRipple; + } + buttonSkip: 6px; + buttonTop: 14px; + position: point(37px, 14px); + icon: icon{{ "emoji/premium_lock", storiesComposeGrayIcon, point(13px, 14px) }}; + } } storiesViewsMenu: PopupMenu(storiesPopupMenuWithIcons) { scrollPadding: margins(0px, 6px, 0px, 4px); diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index a64e004c68..a20a15be37 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1322,6 +1322,7 @@ void ShowPremiumPromoToast( .text = std::move(textWithLink), .st = &st::defaultMultilineToast, .duration = Ui::Toast::kDefaultDuration * 2, + .adaptive = true, .multiline = true, .filter = crl::guard(&show->session(), [=]( const ClickHandlerPtr &, diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 5b97d8cb41..4134c5be9e 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -35,6 +35,14 @@ PremiumCover { about: FlatLabel; additionalShadowForDarkThemes: bool; } +ComposePremiumRequired { + label: FlatLabel; + button: RoundButton; + buttonSkip: pixels; + buttonTop: pixels; + position: point; + icon: icon; +} premiumAboutTextStyle: TextStyle(defaultTextStyle) { font: font(12px);