/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/create_ai_tone_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/emoji_list_widget.h" #include "chat_helpers/stickers_lottie.h" #include "data/data_ai_compose_tones.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_forum_icons.h" #include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "history/view/media/history_view_sticker_player.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 "ui/abstract_button.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/custom_emoji_toast_icon.h" #include "ui/controls/warning_tooltip.h" #include "ui/effects/animations.h" #include "ui/layers/generic_box.h" #include "ui/layers/show.h" #include "ui/painter.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/widgets/shadow.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_layers.h" namespace { constexpr auto kAiComposeToneToastDuration = crl::time(4000); void ShowToneToast( std::shared_ptr show, not_null session, const Data::AiComposeTone &tone, bool created) { const auto size = QSize( st::aiComposeToneToastIconSize.width(), st::aiComposeToneToastIconSize.height()); show->showToast(Ui::Toast::Config{ .title = (created ? tr::lng_ai_compose_tone_created : tr::lng_ai_compose_tone_updated)( tr::now, lt_title, tone.title), .text = tr::lng_ai_compose_tone_created_description( tr::now, Ui::Text::WithEntities), .iconContent = Ui::MakeCustomEmojiToastIcon( session, tone.emojiId, size), .iconPadding = st::aiComposeToneToastIconPadding, .duration = kAiComposeToneToastDuration, }); } void ChooseToneIconBox( not_null box, not_null controller, Fn chosen) { using namespace ChatHelpers; box->setTitle(tr::lng_ai_compose_tone_icon_title()); box->setWidth(st::boxWideWidth); box->setMaxHeight(st::editTopicMaxHeight); box->setScrollStyle(st::reactPanelScroll); const auto manager = &controller->session().data().customEmojiManager(); const auto icons = &controller->session().data().forumIcons(); auto factory = [=](DocumentId id, Fn repaint) -> std::unique_ptr { return manager->create( id, std::move(repaint), Data::CustomEmojiManager::SizeTag::Large); }; const auto top = box->setPinnedToTopContent( object_ptr(box)); const auto body = box->verticalLayout(); const auto selector = body->add( object_ptr(body, EmojiListDescriptor{ .show = controller->uiShow(), .mode = EmojiListWidget::Mode::TopicIcon, .paused = Window::PausedIn( controller, Window::GifPauseReason::Layer), .customRecentList = DocumentListToRecent(icons->list()), .customRecentFactory = std::move(factory), .st = &st::reactPanelEmojiPan, }), st::reactPanelEmojiPan.padding); icons->requestDefaultIfUnknown(); icons->defaultUpdates( ) | rpl::on_next([=] { selector->provideRecent(DocumentListToRecent(icons->list())); }, selector->lifetime()); top->add(selector->createFooter()); const auto shadow = Ui::CreateChild(box.get()); shadow->show(); rpl::combine( top->heightValue(), selector->widthValue() ) | rpl::on_next([=](int topHeight, int width) { shadow->setGeometry(0, topHeight, width, st::lineWidth); }, shadow->lifetime()); selector->refreshEmoji(); selector->scrollToRequests( ) | rpl::on_next([=](int y) { box->scrollToY(y); shadow->update(); }, selector->lifetime()); rpl::combine( box->heightValue(), top->heightValue(), rpl::mappers::_1 - rpl::mappers::_2 ) | rpl::on_next([=](int height) { selector->setMinimalHeight(selector->width(), height); }, body->lifetime()); selector->customChosen( ) | rpl::on_next([=](ChatHelpers::FileChosen data) { chosen(data.document->id); box->closeBox(); }, selector->lifetime()); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } } // namespace not_null AddAiToneIconPreview( not_null container, not_null session, rpl::producer emojiIdValue, Fn emojiIdChosen) { using StickerPlayer = HistoryView::StickerPlayer; struct State { DocumentId emojiId = 0; std::shared_ptr player; bool playerUsesTextColor = false; }; const auto outer = st::aiToneIconPreviewSize; const auto inner = st::aiToneIconPreviewInnerSize; const auto top = st::aiToneIconPreviewTopSkip; const auto bottom = st::aiToneIconPreviewBottomSkip; const auto holder = container->add( object_ptr( container, outer + top + bottom)); const auto button = Ui::CreateChild(holder); button->resize(outer, outer); button->show(); holder->widthValue( ) | rpl::on_next([=](int width) { button->move((width - outer) / 2, top); }, button->lifetime()); const auto state = button->lifetime().make_state(); const auto emojiIdVar = button->lifetime().make_state< rpl::variable>(std::move(emojiIdValue)); emojiIdVar->value( ) | rpl::on_next([=](DocumentId id) { state->emojiId = id; }, button->lifetime()); emojiIdVar->value( ) | rpl::map([=](DocumentId id) -> rpl::producer { if (!id) { return rpl::single((DocumentData*)nullptr); } return session->data().customEmojiManager().resolve( id ) | rpl::map([=](not_null document) { return document.get(); }) | rpl::map_error_to_done(); }) | rpl::flatten_latest( ) | rpl::map([=](DocumentData *document) -> rpl::producer> { if (!document) { return rpl::single(std::shared_ptr()); } const auto media = document->createMediaView(); media->checkStickerLarge(); media->goodThumbnailWanted(); return rpl::single() | rpl::then( document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::take(1) | rpl::map([=] { auto result = std::shared_ptr(); const auto sticker = document->sticker(); const auto size = QSize(inner, inner); if (sticker && sticker->isLottie()) { result = std::make_shared( ChatHelpers::LottiePlayerFromDocument( media.get(), ChatHelpers::StickerLottieSize::StickerSet, size, Lottie::Quality::High)); } else if (sticker && sticker->isWebm()) { result = std::make_shared( media->owner()->location(), media->bytes(), size); } else { result = std::make_shared( media->owner()->location(), media->bytes(), size); } result->setRepaintCallback([=] { button->update(); }); state->playerUsesTextColor = media->owner()->emojiUsesTextColor(); return result; }); }) | rpl::flatten_latest( ) | rpl::on_next([=](std::shared_ptr player) { state->player = std::move(player); button->update(); }, button->lifetime()); button->paintRequest( ) | rpl::on_next([=] { auto p = QPainter(button); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::aiToneIconPreviewBg); p.drawEllipse(button->rect()); if (state->player && state->player->ready()) { const auto color = state->playerUsesTextColor ? st::windowFg->c : QColor(0, 0, 0, 0); const auto frame = state->player->frame( QSize(inner, inner), color, false, crl::now(), false).image; const auto sz = frame.size() / style::DevicePixelRatio(); p.drawImage( QRect( (outer - sz.width()) / 2, (outer - sz.height()) / 2, sz.width(), sz.height()), frame); state->player->markFrameShown(); } else if (!state->emojiId) { st::aiToneIconPreviewPlaceholder.paintInCenter( p, button->rect()); } }, button->lifetime()); if (emojiIdChosen) { button->setClickedCallback([=] { const auto controller = ChatHelpers::ResolveWindowDefault()( session); if (!controller) { return; } controller->uiShow()->showBox(Box( ChooseToneIconBox, controller, crl::guard(button, [=](DocumentId id) { emojiIdChosen(id); }))); }); } else { button->setAttribute(Qt::WA_TransparentForMouseEvents); } return button; } namespace { void SetupToneBox( not_null box, not_null session, DocumentId initialEmojiId, const QString &initialName, const QString &initialPrompt, bool initialDisplayAuthor, rpl::producer title, rpl::producer submitLabel, Fn submit, Fn requestDelete = nullptr) { box->setStyle(st::aiComposeBox); box->setNoContentMargin(true); box->setWidth(st::boxWideWidth); box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); }); box->setTitle(std::move(title)); const auto container = box->verticalLayout(); const auto emojiId = container->lifetime().make_state< rpl::variable>(initialEmojiId); const auto iconButton = AddAiToneIconPreview( container, session, emojiId->value(), [=](DocumentId id) { *emojiId = id; }); const auto name = box->addRow( object_ptr( box, st::aiToneNameField, Ui::InputField::Mode::SingleLine, rpl::producer(), initialName), st::aiToneFieldsMargin); name->setMaxLength(session->appConfig().get( u"aicompose_tone_title_length_max"_q, 12)); Ui::AddSkip(container, st::aiToneFieldsSkip); const auto promptSt = box->lifetime().make_state( st::aiTonePromptField); { const auto &placeholderStyle = st::aiTonePlaceholderLabel.style; const auto fieldsMargin = st::aiToneFieldsMargin; const auto contentWidth = st::boxWideWidth - fieldsMargin.left() - fieldsMargin.right() - promptSt->textMargins.left() - promptSt->textMargins.right(); auto measure = Ui::Text::String{ contentWidth / 2 }; measure.setText( placeholderStyle, tr::lng_ai_compose_tone_prompt_placeholder(tr::now)); const auto desiredMin = measure.countHeight(contentWidth) + promptSt->textMargins.top() + promptSt->textMargins.bottom(); if (promptSt->heightMin < desiredMin) { promptSt->heightMin = desiredMin; } if (promptSt->heightMax < promptSt->heightMin) { promptSt->heightMax = promptSt->heightMin; } } const auto prompt = box->addRow( object_ptr( box, *promptSt, Ui::InputField::Mode::MultiLine, rpl::producer(), initialPrompt), st::aiToneFieldsMargin); prompt->setSubmitSettings(Ui::InputField::SubmitSettings::None); prompt->setMaxLength(session->appConfig().get( u"aicompose_tone_prompt_length_max"_q, 1024)); struct FieldDecor { not_null bg; not_null placeholder; Ui::Animations::Simple anim; bool hidden = false; }; const auto makeDecor = [=]( not_null field, rpl::producer placeholderText) { const auto parent = field->parentWidget(); const auto decor = field->lifetime().make_state(FieldDecor{ .bg = Ui::CreateChild(parent), .placeholder = Ui::CreateChild( parent, std::move(placeholderText), st::aiTonePlaceholderLabel), }); decor->bg->setAttribute(Qt::WA_TransparentForMouseEvents); decor->placeholder->setAttribute(Qt::WA_TransparentForMouseEvents); decor->bg->paintRequest( ) | rpl::on_next([bg = decor->bg] { auto p = QPainter(bg); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::aiToneFieldBg); const auto r = st::aiToneFieldRadius; p.drawRoundedRect(bg->rect(), r, r); }, decor->bg->lifetime()); decor->bg->lower(); decor->placeholder->raise(); const auto applyPosition = [=] { const auto pad = st::aiToneFieldPadding; const auto progress = decor->anim.value(decor->hidden ? 1. : 0.); const auto shift = int(base::SafeRound( progress * (-st::defaultInputField.placeholderShift))); decor->placeholder->moveToLeft( field->x() + pad.left() + shift, field->y() + pad.top()); decor->placeholder->setOpacity(1. - progress); }; field->geometryValue( ) | rpl::on_next([=](QRect g) { if (g.isEmpty()) { return; } const auto pad = st::aiToneFieldPadding; decor->bg->setGeometry(g); decor->placeholder->resizeToWidth( g.width() - pad.left() - pad.right()); applyPosition(); }, field->lifetime()); const auto animate = [=](bool hidden) { if (decor->hidden == hidden) { return; } decor->hidden = hidden; decor->anim.start( applyPosition, hidden ? 0. : 1., hidden ? 1. : 0., st::defaultInputField.duration); }; field->changes( ) | rpl::on_next([=] { animate(!field->getLastText().isEmpty()); }, field->lifetime()); decor->hidden = !field->getLastText().isEmpty(); applyPosition(); return decor; }; makeDecor(name, tr::lng_ai_compose_tone_name_placeholder()); const auto promptDecor = makeDecor( prompt, tr::lng_ai_compose_tone_prompt_placeholder()); const auto authorCheckbox = box->addRow( object_ptr( box, tr::lng_ai_compose_tone_author(tr::now), st::aiComposeEmojifyCheckbox, std::make_unique( st::defaultCheck, initialDisplayAuthor)), st::aiToneAuthorCheckboxMargin, style::al_top); const auto deleteButton = requestDelete ? box->addRow( object_ptr( box, tr::lng_ai_compose_tone_delete(), st::aiToneDeleteButton), st::aiToneDeleteButtonMargin) : nullptr; if (deleteButton) { deleteButton->setFullRadius(true); deleteButton->setClickedCallback(std::move(requestDelete)); box->widthValue( ) | rpl::on_next([=](int width) { const auto &margin = st::aiToneDeleteButtonMargin; deleteButton->setFullWidth( width - margin.left() - margin.right()); }, deleteButton->lifetime()); } rpl::combine( prompt->topValue(), promptDecor->placeholder->heightValue(), box->getDelegate()->contentHeightMaxValue() ) | rpl::on_next([=](int top, int phHeight, int contentHeight) { const auto pad = st::aiToneFieldPadding; const auto deleteBlock = deleteButton ? (deleteButton->heightNoMargins() + st::aiToneDeleteButtonMargin.top() + st::aiToneDeleteButtonMargin.bottom()) : 0; prompt->setMaxHeight(contentHeight - top - st::aiToneFieldsMargin.bottom() - authorCheckbox->heightNoMargins() - st::aiToneAuthorCheckboxMargin.top() - st::aiToneAuthorCheckboxMargin.bottom() - deleteBlock); prompt->setMinHeight(phHeight + pad.top() + pad.bottom()); }, prompt->lifetime()); box->setFocusCallback([=] { name->setFocusFast(); }); const auto warning = box->lifetime().make_state(); const auto save = [=] { const auto nameText = name->getLastText().trimmed(); const auto promptText = prompt->getLastText().trimmed(); const auto showWarning = [=]( not_null target, rpl::producer text) { warning->show({ .parent = box, .target = target, .text = std::move(text), }); }; if (!emojiId->current()) { showWarning( iconButton, tr::lng_ai_compose_tone_warn_icon(tr::marked)); return; } else if (nameText.isEmpty()) { name->showError(); showWarning( name, tr::lng_ai_compose_tone_warn_name(tr::marked)); return; } else if (promptText.isEmpty()) { prompt->showError(); showWarning( prompt, tr::lng_ai_compose_tone_warn_prompt(tr::marked)); return; } warning->hide(anim::type::normal); submit( emojiId->current(), nameText, promptText, authorCheckbox->checked()); }; const auto submitBtn = box->addButton(std::move(submitLabel), save); submitBtn->setFullRadius(true); } } // namespace void CreateAiToneBox( not_null box, not_null session, Fn saved) { SetupToneBox( box, session, DocumentId(0), QString(), QString(), false, tr::lng_ai_compose_create_tone_title(), tr::lng_ai_compose_tone_create(), [=](DocumentId emojiId, const QString &name, const QString &prompt, bool displayAuthor) { session->data().aiComposeTones().create( name, prompt, emojiId, displayAuthor, crl::guard(box, [=](Data::AiComposeTone tone) { const auto show = box->uiShow(); box->closeBox(); ShowToneToast(show, session, tone, true); if (saved) { saved(tone); } }), crl::guard(box, [=](const MTP::Error &error) { if (error.type() == u"TONES_SAVED_TOO_MANY"_q) { ShowAiComposeToneLimitError(box->uiShow(), session); } else if (!MTP::IgnoreError(error)) { box->showToast(error.type()); } })); }, nullptr); } void EditAiToneBox( not_null box, not_null session, const Data::AiComposeTone &tone, Fn saved) { const auto toneCopy = tone; SetupToneBox( box, session, tone.emojiId, tone.title, tone.prompt, tone.authorId != 0, tr::lng_ai_compose_edit_tone_title(), tr::lng_ai_compose_tone_save(), [=](DocumentId emojiId, const QString &name, const QString &prompt, bool displayAuthor) { session->data().aiComposeTones().update( toneCopy, name, prompt, std::make_optional(emojiId), std::make_optional(displayAuthor), crl::guard(box, [=](Data::AiComposeTone updated) { const auto show = box->uiShow(); box->closeBox(); ShowToneToast(show, session, updated, false); if (saved) { saved(updated); } })); }, [=] { ConfirmDeleteAiTone( box->uiShow(), session, toneCopy, [=] { box->closeBox(); }); }); } void ConfirmDeleteAiTone( std::shared_ptr show, not_null session, const Data::AiComposeTone &tone, Fn done) { if (!tone.creator) { show->show(Ui::MakeConfirmBox({ .text = tr::lng_ai_compose_tone_remove_sure(), .confirmed = [=](Fn &&close) { close(); session->data().aiComposeTones().save( tone, true, done); }, .confirmText = tr::lng_box_remove(), })); return; } show->show(Ui::MakeConfirmBox({ .text = tr::lng_ai_compose_tone_delete_sure(), .confirmed = [=](Fn &&close) { close(); session->data().aiComposeTones().remove(tone, done); }, .confirmText = tr::lng_box_delete(), .confirmStyle = &st::attentionBoxButton, .title = tr::lng_ai_compose_tone_delete(), })); } void ShowAiComposeToneLimitError( std::shared_ptr show, not_null session) { const auto limits = Data::PremiumLimits(session); const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); const auto defaultLimit = limits.aiComposeSavedTonesDefault(); const auto premiumLimit = limits.aiComposeSavedTonesPremium(); const auto current = premium ? premiumLimit : defaultLimit; if (premium || !premiumPossible) { show->showToast(tr::lng_ai_compose_tone_saved_limit_final( tr::now, lt_count, current, tr::rich)); } else { Settings::ShowPremiumPromoToast( Main::MakeSessionShow(show, session), ChatHelpers::ResolveWindowDefault(), tr::lng_ai_compose_tone_saved_limit( tr::now, lt_count, defaultLimit, lt_link, tr::bold(tr::lng_ai_compose_tone_saved_limit_link( tr::now, tr::link)), lt_premium_count, tr::bold(QString::number(premiumLimit)), tr::rich), u"ai_compose_tones"_q); } }