diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 740e34464a..b2b55ce294 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2964,6 +2964,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_gifts" = "Gifts are collectible items you can trade or showcase on your profile."; "lng_premium_summary_subtitle_no_forwards" = "Disable Sharing"; "lng_premium_summary_about_no_forwards" = "Restrict forwarding, copying, and saving content from your private chats."; +"lng_premium_summary_subtitle_ai_compose" = "AI Tools"; +"lng_premium_summary_about_ai_compose" = "Transform your messages and entire chats in your preferred style and language."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; @@ -7772,8 +7774,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_result" = "Result"; "lng_ai_compose_to_language" = "To {language}"; "lng_ai_compose_emojify" = "emojify"; -"lng_ai_compose_loading" = "Working on it..."; "lng_ai_compose_error" = "AI request failed."; "lng_ai_compose_tooltip" = "Rewrite, translate, or correct your text using AI."; +"lng_ai_compose_flood_title" = "Daily limit reached"; +"lng_ai_compose_flood_text" = "Get {link} for **50x** more AI text transformations per day."; +"lng_ai_compose_flood_link" = "Telegram Premium"; +"lng_ai_compose_increase_limit" = "Increase Limit"; +"lng_ai_compose_select_style" = "Select Style"; +"lng_ai_compose_apply_style" = "Apply Style"; // Keys finished diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 6c4636b287..110d040112 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1309,6 +1309,23 @@ aiComposeStyleEmojiTop: 5px; aiComposeStyleLabelTop: 30px; aiComposeStyleEmojiFont: font(20px); aiComposeStyleLabelFont: font(12px semibold); +aiComposeStyleTabsScroll: ScrollArea(defaultScrollArea) { + barHidden: true; +} +aiComposeStyleButtonPadding: margins(12px, 0px, 12px, 0px); +aiComposeStyleFadeWidth: 24px; +aiComposeBadge: RoundButton(customEmojiTextBadge) { + textFg: activeButtonBg; + textBg: activeButtonFg; + width: -8px; + height: 16px; + radius: 4px; + textTop: 1px; + style: TextStyle(semiboldTextStyle) { + font: font(10px semibold); + } +} +aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px); aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 7d14d0ba9d..577b5dcc59 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -9,17 +9,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_compose_with_ai.h" #include "apiwrap.h" +#include "boxes/premium_preview_box.h" +#include "chat_helpers/compose/compose_show.h" #include "core/ui_integration.h" #include "lang/lang_instance.h" #include "lang/lang_keys.h" +#include "main/session/session_show.h" #include "main/main_app_config.h" #include "main/main_session.h" +#include "settings/sections/settings_premium.h" #include "spellcheck/platform/platform_language.h" #include "ui/boxes/choose_language_box.h" #include "ui/controls/send_button.h" #include "ui/effects/ripple_animation.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" +#include "ui/text/custom_emoji_helper.h" +#include "ui/text/custom_emoji_text_badge.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/wrap/slide_wrap.h" @@ -27,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/widgets/scroll_area.h" #include "styles/style_basic.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" @@ -35,6 +42,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +#include + namespace HistoryView::Controls { namespace { @@ -235,8 +244,9 @@ public: Main::AppConfig::AiComposeStyle style); void setSelected(bool selected); + void setExtraPadding(int extra); [[nodiscard]] const Main::AppConfig::AiComposeStyle &style() const; - + protected: void paintEvent(QPaintEvent *e) override; [[nodiscard]] QImage prepareRippleMask() const override; @@ -245,6 +255,7 @@ private: const Main::AppConfig::AiComposeStyle _style; const QString _label; bool _selected = false; + int _extraPadding = 0; }; @@ -256,16 +267,40 @@ public: void setChangedCallback(Fn callback); void setActive(int index); + void resizeForOuterWidth(int outerWidth); [[nodiscard]] QString currentTone() const; + [[nodiscard]] int buttonCount() const; protected: - int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; private: - QList> _buttons; + std::vector> _buttons; Fn _changed; - int _active = 0; + int _active = -1; + +}; + +class ComposeAiStyleScrollTabs final : public Ui::RpWidget { +public: + ComposeAiStyleScrollTabs( + QWidget *parent, + std::vector styles); + + [[nodiscard]] not_null inner() const; + +protected: + int resizeGetHeight(int newWidth) override; + +private: + void updateFades(); + + const not_null _scroll; + const not_null _inner; + const not_null _fadeLeft; + const not_null _fadeRight; + const not_null _cornerLeft; + const not_null _cornerRight; }; @@ -286,6 +321,7 @@ public: void setEmojifyVisible(bool visible); void setEmojifyChecked(bool checked); void setLoading(bool loading); + void setResultVisible(bool visible); void setResult(TextWithEntities text, bool copyVisible); protected: @@ -313,6 +349,7 @@ private: bool _originalVisible = true; bool _loading = false; bool _hasShownResult = false; + bool _resultVisible = true; bool _copyVisible = false; bool _emojifyVisible = false; bool _dividerVisible = false; @@ -332,8 +369,13 @@ public: [[nodiscard]] const TextWithEntities &result() const; [[nodiscard]] const std::vector &stylesData() const; void setReadyChangedCallback(Fn callback); + void setPremiumFloodCallback(Fn callback); + void setModeChangedCallback(Fn callback); + void setStyleSelectedCallback(Fn callback); + [[nodiscard]] ComposeAiMode mode() const; + [[nodiscard]] bool hasStyleSelection() const; void setModeTabs(not_null tabs); - void setStyleTabs(not_null*> stylesWrap); + void setStyleTabs(not_null*> stylesWrap); void start(); protected: @@ -359,12 +401,15 @@ private: LanguageId _to; const std::vector _stylesData; QPointer _tabs; - QPointer _styles; - QPointer> _stylesWrap; + QPointer _styles; + QPointer> _stylesWrap; const not_null _preview; Fn _readyChanged; + Fn _premiumFlood; + Fn _modeChanged; + Fn _styleSelected; ComposeAiMode _mode = ComposeAiMode::Translate; - int _styleIndex = 0; + int _styleIndex = -1; bool _emojify = false; bool _ready = false; mtpRequestId _requestId = 0; @@ -529,6 +574,15 @@ ComposeAiStyleButton::ComposeAiStyleButton( , _style(std::move(style)) , _label(ToneTitle(_style.key)) { setCursor(style::cur_pointer); + setNaturalWidth([&] { + const auto padding = st::aiComposeStyleButtonPadding; + const auto labelWidth = st::aiComposeStyleLabelFont->width(_label); + const auto emojiWidth = st::aiComposeStyleEmojiFont->width(_style.emoji); + return padding.left() + + std::max(labelWidth, emojiWidth) + + padding.right(); + }()); + setExtraPadding(0); } void ComposeAiStyleButton::setSelected(bool selected) { @@ -539,6 +593,11 @@ void ComposeAiStyleButton::setSelected(bool selected) { update(); } +void ComposeAiStyleButton::setExtraPadding(int extra) { + _extraPadding = extra; + resize(naturalWidth() + 2 * extra, height()); +} + const Main::AppConfig::AiComposeStyle &ComposeAiStyleButton::style() const { return _style; } @@ -615,8 +674,9 @@ ComposeAiStyleTabs::ComposeAiStyleTabs( this, std::move(style)); button->setClickedCallback([=] { - const auto index = _buttons.indexOf(button); - if (index < 0) { + const auto i = ranges::find(_buttons, not_null(button)); + const auto index = int(i - begin(_buttons)); + if (index == _buttons.size()) { return; } setActive(index); @@ -626,7 +686,16 @@ ComposeAiStyleTabs::ComposeAiStyleTabs( }); _buttons.push_back(button); } - setActive(0); + setNaturalWidth([&] { + const auto padding = st::aiComposeStyleTabsPadding; + const auto skip = st::aiComposeStyleTabsSkip; + auto total = padding.left(); + for (const auto button : _buttons) { + total += button->naturalWidth() + skip; + } + return total - skip + padding.right(); + }()); + setActive(-1); } void ComposeAiStyleTabs::setChangedCallback(Fn callback) { @@ -634,36 +703,88 @@ void ComposeAiStyleTabs::setChangedCallback(Fn callback) { } void ComposeAiStyleTabs::setActive(int index) { - if (index < 0 || index >= _buttons.size()) { + if (index < -1 || index >= int(_buttons.size())) { return; } _active = index; - for (auto i = 0; i != _buttons.size(); ++i) { + for (auto i = 0; i != int(_buttons.size()); ++i) { _buttons[i]->setSelected(i == index); } } +void ComposeAiStyleTabs::resizeForOuterWidth(int outerWidth) { + const auto count = int(_buttons.size()); + const auto padding = st::aiComposeStyleTabsPadding; + const auto skip = st::aiComposeStyleTabsSkip; + const auto bheight = st::aiComposeStyleTabsHeight + - padding.top() + - padding.bottom(); + const auto height = st::aiComposeStyleTabsHeight; + auto left = padding.left(); + const auto guard = gsl::finally([&] { + resize(left - skip + padding.right(), height); + }); + if (!count) { + return; + } + const auto setExtraPaddingFor = [&]( + not_null button, + int value) { + button->setExtraPadding(value); + + const auto w = button->width(); + button->setGeometry(left, padding.top(), w, bheight); + left += w + skip; + }; + const auto diff = naturalWidth() - outerWidth; + if (diff > 0) { + auto total = left; + for (auto fit = 0; fit != count;) { + const auto width = _buttons[fit]->naturalWidth(); + const auto tooLarge = (total + (width / 2) > outerWidth); + if (!tooLarge) { + ++fit; + total += width + skip; + } + if (tooLarge || (fit == count)) { + if (fit > 0) { + const auto width = _buttons[fit - 1]->naturalWidth(); + const auto desired = total - skip - (width / 2); + const auto add = outerWidth - desired; + const auto extra = add / ((fit - 1) * 2 + 1); + for (const auto &button : _buttons) { + setExtraPaddingFor(button, extra); + } + } else { + for (const auto &button : _buttons) { + setExtraPaddingFor(button, 0); + } + } + return; + } + } + Unexpected("Tabs width inconsistency."); + } else { + const auto add = -diff / 2; + const auto each = add / _buttons.size(); + const auto more = add - (each * _buttons.size()); + for (auto i = 0; i < more; ++i) { + setExtraPaddingFor(_buttons[i], each + 1); + } + for (auto i = more; i < count; ++i) { + setExtraPaddingFor(_buttons[i], each); + } + } +} + QString ComposeAiStyleTabs::currentTone() const { - return (_active >= 0 && _active < _buttons.size()) + return (_active >= 0 && _active < int(_buttons.size())) ? _buttons[_active]->style().key : QString(); } -int ComposeAiStyleTabs::resizeGetHeight(int newWidth) { - const auto padding = st::aiComposeStyleTabsPadding; - const auto skip = st::aiComposeStyleTabsSkip; - const auto count = std::max(_buttons.size(), 1); - const auto innerWidth = newWidth - padding.left() - padding.right(); - const auto width = (innerWidth - ((count - 1) * skip)) / count; - const auto height = st::aiComposeStyleTabsHeight - - padding.top() - - padding.bottom(); - auto left = padding.left(); - for (const auto button : _buttons) { - button->setGeometry(left, padding.top(), width, height); - left += width + skip; - } - return st::aiComposeStyleTabsHeight; +int ComposeAiStyleTabs::buttonCount() const { + return int(_buttons.size()); } void ComposeAiStyleTabs::paintEvent(QPaintEvent *e) { @@ -678,6 +799,130 @@ void ComposeAiStyleTabs::paintEvent(QPaintEvent *e) { radius); } +ComposeAiStyleScrollTabs::ComposeAiStyleScrollTabs( + QWidget *parent, + std::vector styles) +: RpWidget(parent) +, _scroll(Ui::CreateChild( + this, + st::aiComposeStyleTabsScroll)) +, _inner(_scroll->setOwnedWidget( + object_ptr(this, std::move(styles)))) +, _fadeLeft(Ui::CreateChild(this)) +, _fadeRight(Ui::CreateChild(this)) +, _cornerLeft(Ui::CreateChild(this)) +, _cornerRight(Ui::CreateChild(this)) { + _scroll->setCustomWheelProcess([=](not_null e) { + const auto pixelDelta = e->pixelDelta(); + const auto angleDelta = e->angleDelta(); + if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) { + return false; + } + const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y(); + _scroll->scrollToX(_scroll->scrollLeft() - y); + return true; + }); + + const auto setupFade = [&]( + not_null fade, + bool left) { + fade->setAttribute(Qt::WA_TransparentForMouseEvents); + fade->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(fade); + const auto w = fade->width(); + const auto bg = st::aiComposeStyleTabsBg->c; + auto transparent = bg; + transparent.setAlpha(0); + auto gradient = QLinearGradient(0, 0, w, 0); + if (left) { + gradient.setColorAt(0., bg); + gradient.setColorAt(1., transparent); + } else { + gradient.setColorAt(0., transparent); + gradient.setColorAt(1., bg); + } + p.fillRect(fade->rect(), gradient); + }, fade->lifetime()); + fade->hide(); + }; + setupFade(_fadeLeft, true); + setupFade(_fadeRight, false); + + const auto setupCorner = [&]( + not_null corner, + bool left) { + corner->setAttribute(Qt::WA_TransparentForMouseEvents); + corner->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(corner); + PainterHighQualityEnabler hq(p); + const auto w = corner->width(); + const auto h = corner->height(); + auto mask = QPainterPath(); + mask.addRect(0, 0, w, h); + auto rounded = QPainterPath(); + if (left) { + rounded.addRoundedRect(0, 0, w * 2, h, w, w); + } else { + rounded.addRoundedRect(-w, 0, w * 2, h, w, w); + } + p.setPen(Qt::NoPen); + p.setBrush(st::boxDividerBg); + p.drawPath(mask.subtracted(rounded)); + }, corner->lifetime()); + }; + setupCorner(_cornerLeft, true); + setupCorner(_cornerRight, false); + + _scroll->scrolls( + ) | rpl::on_next([=] { + updateFades(); + }, lifetime()); + + rpl::combine( + widthValue(), + _scroll->widthValue(), + _inner->widthValue() + ) | rpl::on_next([=] { + updateFades(); + }, lifetime()); +} + +not_null ComposeAiStyleScrollTabs::inner() const { + return _inner; +} + +int ComposeAiStyleScrollTabs::resizeGetHeight(int newWidth) { + _scroll->setGeometry(0, 0, newWidth, st::aiComposeStyleTabsHeight); + + _inner->resizeForOuterWidth(newWidth); + + const auto fadeWidth = st::aiComposeStyleFadeWidth; + const auto fadeHeight = st::aiComposeStyleTabsHeight; + _fadeLeft->setGeometry(0, 0, fadeWidth, fadeHeight); + _fadeRight->setGeometry( + newWidth - fadeWidth, 0, fadeWidth, fadeHeight); + _fadeLeft->raise(); + _fadeRight->raise(); + + const auto radius = st::aiComposeStyleTabsRadius; + _cornerLeft->setGeometry(0, 0, radius, fadeHeight); + _cornerRight->setGeometry(newWidth - radius, 0, radius, fadeHeight); + _cornerLeft->raise(); + _cornerRight->raise(); + + updateFades(); + return st::aiComposeStyleTabsHeight; +} + +void ComposeAiStyleScrollTabs::updateFades() { + const auto scrollLeft = _scroll->scrollLeft(); + const auto scrollMax = _scroll->scrollLeftMax(); + _fadeLeft->setVisible(scrollLeft > 0); + _fadeRight->setVisible(scrollLeft < scrollMax); +} + // ComposeAiPreviewCard ComposeAiPreviewCard::ComposeAiPreviewCard( @@ -795,14 +1040,20 @@ void ComposeAiPreviewCard::setEmojifyChecked(bool checked) { void ComposeAiPreviewCard::setLoading(bool loading) { _loading = loading; if (loading && !_hasShownResult) { - _resultBody->setMarkedText( - Ui::Text::Italic(tr::lng_ai_compose_loading(tr::now)), - _context); + _resultBody->setMarkedText(_original, _context); } _copy->setVisible(!loading && _copyVisible); refreshGeometry(); } +void ComposeAiPreviewCard::setResultVisible(bool visible) { + if (_resultVisible == visible) { + return; + } + _resultVisible = visible; + refreshGeometry(); +} + void ComposeAiPreviewCard::setResult(TextWithEntities text, bool copyVisible) { _loading = false; _hasShownResult = true; @@ -863,47 +1114,60 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { _originalToggle->hide(); } - auto controlsWidth = 0; - if (_emojifyVisible) { - _emojify->show(); - _emojify->resizeToNaturalWidth(contentWidth); - controlsWidth += _emojify->width() + st::aiComposeCardControlSkip; + if (_resultVisible) { + _resultTitle->show(); + _resultBody->show(); + auto controlsWidth = 0; + if (_emojifyVisible) { + _emojify->show(); + _emojify->resizeToNaturalWidth(contentWidth); + controlsWidth += _emojify->width() + + st::aiComposeCardControlSkip; + } else { + _emojify->hide(); + } + const auto resultTitleWidth = std::max( + contentWidth - controlsWidth, + 0); + _resultTitle->resizeToWidth(resultTitleWidth); + auto right = padding.right(); + if (_emojifyVisible) { + _emojify->moveToRight(right, y, newWidth); + right += _emojify->width() + st::aiComposeCardControlSkip; + } + _resultTitle->setGeometryToLeft( + padding.left(), + y, + resultTitleWidth, + _resultTitle->height(), + newWidth); + y += std::max({ + _resultTitle->height(), + _emojifyVisible ? _emojify->height() : 0, + }); + y += st::aiComposeCardTextSkip; + + _resultBody->resizeToWidth(contentWidth); + _resultBody->setGeometryToLeft( + padding.left(), + y, + contentWidth, + _resultBody->height(), + newWidth); + y += _resultBody->height(); + + if (_copy->isVisible()) { + y += st::aiComposeCardCopySkip; + _copy->moveToRight(padding.right(), y, newWidth); + y += _copy->height(); + } + } else { + _resultTitle->hide(); + _resultBody->hide(); + _copy->hide(); _emojify->hide(); } - const auto resultTitleWidth = std::max(contentWidth - controlsWidth, 0); - _resultTitle->resizeToWidth(resultTitleWidth); - auto right = padding.right(); - if (_emojifyVisible) { - _emojify->moveToRight(right, y, newWidth); - right += _emojify->width() + st::aiComposeCardControlSkip; - } - _resultTitle->setGeometryToLeft( - padding.left(), - y, - resultTitleWidth, - _resultTitle->height(), - newWidth); - y += std::max({ - _resultTitle->height(), - _emojifyVisible ? _emojify->height() : 0, - }); - y += st::aiComposeCardTextSkip; - - _resultBody->resizeToWidth(contentWidth); - _resultBody->setGeometryToLeft( - padding.left(), - y, - contentWidth, - _resultBody->height(), - newWidth); - y += _resultBody->height(); - - if (_copy->isVisible()) { - y += st::aiComposeCardCopySkip; - _copy->moveToRight(padding.right(), y, newWidth); - y += _copy->height(); - } return y + padding.bottom(); } @@ -997,19 +1261,24 @@ void ComposeAiContent::setModeTabs(not_null tabs) { } void ComposeAiContent::setStyleTabs( - not_null*> stylesWrap) { + not_null*> stylesWrap) { _stylesWrap = stylesWrap; _stylesWrap->setDuration(0); _styles = stylesWrap->entity(); - _styles->setChangedCallback([=](int index) { - if (index >= 0 && index < _stylesData.size()) { + _styles->inner()->setChangedCallback([=](int index) { + if (index >= 0 && index < int(_stylesData.size())) { + const auto wasNoSelection = (_styleIndex < 0); _styleIndex = index; + updateTitles(); if (_mode == ComposeAiMode::Style) { request(); + if (wasNoSelection && _styleSelected) { + _styleSelected(); + } } } }); - _styles->setActive(_styleIndex); + _styles->inner()->setActive(_styleIndex); _stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant); } @@ -1067,6 +1336,9 @@ void ComposeAiContent::setMode(ComposeAiMode mode) { return; } _mode = mode; + if (_modeChanged) { + _modeChanged(_mode); + } updatePinnedTabs(anim::type::normal); updateTitles(); refreshLayout(); @@ -1074,7 +1346,11 @@ void ComposeAiContent::setMode(ComposeAiMode mode) { } void ComposeAiContent::updateTitles() { - _preview->setOriginalVisible(_mode != ComposeAiMode::Style); + const auto styleNoSelection = (_mode == ComposeAiMode::Style) + && (_styleIndex < 0); + _preview->setOriginalVisible( + _mode != ComposeAiMode::Style || styleNoSelection); + _preview->setResultVisible(!styleNoSelection); _preview->setOriginalTitle( (_mode == ComposeAiMode::Translate) ? FromTitle(_detectedFrom) @@ -1092,7 +1368,7 @@ void ComposeAiContent::updatePinnedTabs(anim::type animated) { _tabs->setActive(_mode); } if (_styles) { - _styles->setActive(_styleIndex); + _styles->inner()->setActive(_styleIndex); } if (_stylesWrap) { _stylesWrap->toggle(_mode == ComposeAiMode::Style, animated); @@ -1108,6 +1384,9 @@ void ComposeAiContent::cancelRequest() { } void ComposeAiContent::request() { + if (_mode == ComposeAiMode::Style && _styleIndex < 0) { + return; + } cancelRequest(); _ready = false; _result = {}; @@ -1168,6 +1447,24 @@ void ComposeAiContent::showError(const MTP::Error &error) { _ready = false; notifyReadyChanged(); refreshLayout(); + if (error.type() == u"AICOMPOSE_FLOOD_PREMIUM"_q) { + const auto show = Main::MakeSessionShow( + _box->uiShow(), + _session); + Settings::ShowPremiumPromoToast( + show, + ChatHelpers::ResolveWindowDefault(), + tr::lng_ai_compose_flood_text( + tr::now, + lt_link, + tr::link(tr::lng_ai_compose_flood_link(tr::now)), + tr::rich), + u"ai_compose"_q); + if (_premiumFlood) { + _premiumFlood(); + } + return; + } _box->showToast(error.type().isEmpty() ? tr::lng_ai_compose_error(tr::now) : error.type()); @@ -1179,10 +1476,31 @@ void ComposeAiContent::notifyReadyChanged() { } } +void ComposeAiContent::setPremiumFloodCallback(Fn callback) { + _premiumFlood = std::move(callback); +} + +void ComposeAiContent::setModeChangedCallback( + Fn callback) { + _modeChanged = std::move(callback); +} + +void ComposeAiContent::setStyleSelectedCallback(Fn callback) { + _styleSelected = std::move(callback); +} + +ComposeAiMode ComposeAiContent::mode() const { + return _mode; +} + +bool ComposeAiContent::hasStyleSelection() const { + return _styleIndex >= 0; +} + } // namespace void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { - box->setStyle(st::aiComposeBoxWithSend); + box->setStyle(st::aiComposeBox); box->setNoContentMargin(true); box->setWidth(st::boxWideWidth); box->setTitle(tr::lng_ai_compose_title()); @@ -1190,27 +1508,33 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { box->closeBox(); }); + const auto session = args.session; const auto body = box->verticalLayout(); - Ui::AddSkip(body, st::aiComposeBoxSectionSkip); + const auto tabsSkip = QMargins(0, 0, 0, st::aiComposeBoxStyleTabsSkip); const auto pinnedToTop = box->setPinnedToTopContent( object_ptr(box)); const auto tabs = pinnedToTop->add( object_ptr(pinnedToTop), - st::aiComposeContentMargin); + st::aiComposeContentMargin + tabsSkip); const auto content = body->add( object_ptr(box, box, args), st::aiComposeContentMargin); const auto stylesWrap = pinnedToTop->add( - object_ptr>( + object_ptr>( pinnedToTop, - object_ptr( + object_ptr( pinnedToTop, content->stylesData()), - style::margins(0, st::aiComposeBoxStyleTabsSkip, 0, 0)), + tabsSkip), st::aiComposeContentMargin); stylesWrap->hide(anim::type::instant); content->setModeTabs(tabs); content->setStyleTabs(stylesWrap); + + auto premiumFlooded = std::make_shared(false); + auto applyButton = std::make_shared>(); + auto sendButton = std::make_shared>(); + const auto applyAndClose = [=] { if (!content->hasResult()) { return; @@ -1218,36 +1542,111 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { args.apply(TextWithEntities(content->result())); box->closeBox(); }; - const auto apply = box->addButton( - tr::lng_ai_compose_apply(), - applyAndClose); - apply->setDisabled(!content->hasResult()); - apply->setFullRadius(true); - const auto send = Ui::CreateChild( - apply->parentWidget(), - st::aiComposeSendButton); - send->setState({ .type = Ui::SendButton::Type::Send }); - send->show(); - send->setAttribute(Qt::WA_TransparentForMouseEvents, !content->hasResult()); + const auto rebuildButtons = [=] { + if (*sendButton) { + delete sendButton->data(); + } + *sendButton = nullptr; + box->clearButtons(); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + *applyButton = nullptr; - apply->geometryValue( - ) | rpl::on_next([=](QRect geometry) { - const auto size = st::aiComposeBoxWithSend.buttonHeight; - send->resize(size, size); - send->moveToLeft( - geometry.x() + geometry.width() + st::aiComposeSendButtonSkip, - geometry.y() + (geometry.height() - size) / 2); - }, send->lifetime()); + if (*premiumFlooded) { + box->setStyle(st::aiComposeBox); + auto helper = Ui::Text::CustomEmojiHelper(); + const auto badge = helper.paletteDependent( + Ui::Text::CustomEmojiTextBadge( + u"x50"_q, + st::aiComposeBadge, + st::aiComposeBadgeMargin)); + const auto btn = box->addButton( + tr::lng_ai_compose_increase_limit(), nullptr); + btn->setFullRadius(true); + btn->setContext(helper.context()); + btn->setText(rpl::single( + tr::lng_ai_compose_increase_limit(tr::now, tr::marked) + .append(' ') + .append(badge))); + const auto resolve = ChatHelpers::ResolveWindowDefault(); + const auto close = crl::guard(box, [=] { + box->closeBox(); + }); + btn->setClickedCallback([=] { + if (const auto controller = resolve(session)) { + ShowPremiumPreviewBox( + controller, + PremiumFeature::AiCompose); + } + close(); + }); + } else if (content->mode() == ComposeAiMode::Style + && !content->hasStyleSelection()) { + box->setStyle(st::aiComposeBox); + const auto btn = box->addButton( + tr::lng_ai_compose_select_style(), nullptr); + btn->setFullRadius(true); + btn->setDisabled(true); + btn->setTextFgOverride( + anim::color(st::activeButtonBg, st::activeButtonFg, 0.5)); + } else { + box->setStyle(st::aiComposeBoxWithSend); + const auto isStyle = + (content->mode() == ComposeAiMode::Style); + const auto btn = box->addButton( + isStyle + ? tr::lng_ai_compose_apply_style() + : tr::lng_ai_compose_apply(), + applyAndClose); + btn->setFullRadius(true); + btn->setDisabled(!content->hasResult()); + *applyButton = btn; - send->setClickedCallback(applyAndClose); + const auto send = Ui::CreateChild( + btn->parentWidget(), + st::aiComposeSendButton); + send->setState({ .type = Ui::SendButton::Type::Send }); + send->show(); + send->setAttribute( + Qt::WA_TransparentForMouseEvents, + !content->hasResult()); + btn->geometryValue( + ) | rpl::on_next([=](QRect geometry) { + const auto size = st::aiComposeBoxWithSend.buttonHeight; + send->resize(size, size); + send->moveToLeft( + geometry.x() + geometry.width() + + st::aiComposeSendButtonSkip, + geometry.y() + (geometry.height() - size) / 2); + }, send->lifetime()); + send->setClickedCallback(applyAndClose); + *sendButton = send; + } + }; content->setReadyChangedCallback([=](bool ready) { - if (apply) { - apply->setDisabled(!ready); + if (*applyButton) { + (*applyButton)->setDisabled(!ready); + } + if (*sendButton) { + (*sendButton)->setAttribute( + Qt::WA_TransparentForMouseEvents, !ready); } - send->setAttribute(Qt::WA_TransparentForMouseEvents, !ready); }); + content->setPremiumFloodCallback([=] { + *premiumFlooded = true; + rebuildButtons(); + }); + content->setModeChangedCallback([=](ComposeAiMode) { + rebuildButtons(); + }); + content->setStyleSelectedCallback([=] { + rebuildButtons(); + }); + + rebuildButtons(); content->start(); } diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 59ce2da496..ff78f6b373 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -146,6 +146,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_subtitle_gifts(); case PremiumFeature::NoForwards: return tr::lng_premium_summary_subtitle_no_forwards(); + case PremiumFeature::AiCompose: + return tr::lng_premium_summary_subtitle_ai_compose(); case PremiumFeature::BusinessLocation: return tr::lng_business_subtitle_location(); @@ -219,6 +221,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_about_gifts(); case PremiumFeature::NoForwards: return tr::lng_premium_summary_about_no_forwards(); + case PremiumFeature::AiCompose: + return tr::lng_premium_summary_about_ai_compose(); case PremiumFeature::BusinessLocation: return tr::lng_business_about_location(); @@ -563,6 +567,7 @@ struct VideoPreviewDocument { case PremiumFeature::PeerColors: return "peer_colors"; case PremiumFeature::Gifts: return "gifts"; case PremiumFeature::NoForwards: return "no_forwards"; + case PremiumFeature::AiCompose: return "ai_compose"; case PremiumFeature::BusinessLocation: return "business_location"; case PremiumFeature::BusinessHours: return "business_hours"; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index cb3944a4ed..84e14877a8 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -76,6 +76,7 @@ enum class PremiumFeature { PeerColors, Gifts, NoForwards, + AiCompose, // Business features. BusinessLocation, diff --git a/Telegram/SourceFiles/settings/sections/settings_premium.cpp b/Telegram/SourceFiles/settings/sections/settings_premium.cpp index 7e708fa586..2cb26d306e 100644 --- a/Telegram/SourceFiles/settings/sections/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_premium.cpp @@ -256,6 +256,7 @@ using Order = std::vector; u"premium_stickers"_q, u"business"_q, u"effects"_q, + u"ai_compose"_q, }; } @@ -469,6 +470,15 @@ using Order = std::vector; PremiumFeature::NoForwards, }, }, + { + u"ai_compose"_q, + Entry{ + &st::settingsPremiumIconAiCompose, + tr::lng_premium_summary_subtitle_ai_compose(), + tr::lng_premium_summary_about_ai_compose(), + PremiumFeature::AiCompose, + }, + }, }; } @@ -1032,6 +1042,8 @@ void TopBarWithSticker::resizeEvent(QResizeEvent *e) { return tr::lng_premium_summary_subtitle_todo_lists(tr::now); } else if (key == u"no_forwards"_q) { return tr::lng_premium_summary_subtitle_no_forwards(tr::now); + } else if (key == u"ai_compose"_q) { + return tr::lng_premium_summary_subtitle_ai_compose(tr::now); } return QString(); } @@ -2110,6 +2122,8 @@ std::vector PremiumFeaturesOrder( return PremiumFeature::Gifts; } else if (s == u"no_forwards"_q) { return PremiumFeature::NoForwards; + } else if (s == u"ai_compose"_q) { + return PremiumFeature::AiCompose; } return PremiumFeature::kCount; }) | ranges::views::filter([](PremiumFeature type) { diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 51b7386242..2a5249c142 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -106,6 +106,7 @@ settingsPremiumIconEffects: icon {{ "settings/premium/effects", settingsIconFg } settingsPremiumIconChecklist: icon {{ "settings/premium/checklist", settingsIconFg }}; settingsPremiumIconPeerColors: icon {{ "settings/premium/peer_colors-24x24", settingsIconFg }}; settingsPremiumIconNoForwards: icon {{ "menu/share_off-24x24", settingsIconFg }}; +settingsPremiumIconAiCompose: icon {{ "menu/edit_stars", settingsIconFg }}; settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }};