diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fdde7123e2..43684ca368 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1777,6 +1777,8 @@ PRIVATE ui/chat/sponsored_message_bar.h ui/controls/compose_ai_button_factory.cpp ui/controls/compose_ai_button_factory.h + ui/controls/custom_emoji_toast_icon.cpp + ui/controls/custom_emoji_toast_icon.h ui/controls/emoji_button_factory.cpp ui/controls/emoji_button_factory.h ui/controls/location_picker.cpp @@ -1787,6 +1789,8 @@ PRIVATE ui/controls/table_rows.h ui/controls/userpic_button.cpp ui/controls/userpic_button.h + ui/controls/warning_tooltip.cpp + ui/controls/warning_tooltip.h ui/effects/credits_graphics.cpp ui/effects/credits_graphics.h ui/effects/emoji_fly_animation.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4da4aa18fa..67ecbc536e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7937,6 +7937,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tone_saved" = "Style saved."; "lng_ai_compose_tone_link_copied" = "Style link copied."; "lng_ai_compose_author" = "This style was created by {user}"; +"lng_ai_compose_tone_warn_icon" = "Please choose an icon."; +"lng_ai_compose_tone_warn_name" = "Please choose a name."; +"lng_ai_compose_tone_warn_prompt" = "Please enter instructions."; +"lng_ai_compose_tone_created" = "{title} Style Created!"; +"lng_ai_compose_tone_updated" = "{title} Style Updated!"; +"lng_ai_compose_tone_created_description" = "Right click the style to edit or share the link."; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 0920945be4..f69bf8b671 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1288,6 +1288,8 @@ aiTonePlaceholderLabel: FlatLabel(defaultFlatLabel) { } } aiToneAuthorCheckboxMargin: margins(0px, 12px, 0px, 8px); +aiComposeToneToastIconSize: size(32px, 32px); +aiComposeToneToastIconPadding: margins(12px, 6px, 12px, 6px); aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 7ef8e3079d..ef37a8f1fc 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -398,6 +398,7 @@ public: void setModeTabs(not_null tabs); void setStyleTabs(not_null*> stylesWrap); void refreshTones(); + void selectToneById(uint64 id); void start(); protected: @@ -1097,7 +1098,12 @@ void ComposeAiContent::setStyleTabs( } } else if (index == int(_tones.size())) { _styles->setActive(_styleIndex); - _box->uiShow()->show(Box(CreateAiToneBox, _session, nullptr)); + _box->uiShow()->show(Box( + CreateAiToneBox, + _session, + crl::guard(this, [=](Data::AiComposeTone tone) { + selectToneById(tone.id); + }))); } }); _styles->setActive(_styleIndex); @@ -1135,6 +1141,27 @@ void ComposeAiContent::refreshTones() { _styleIndex = remapped; } +void ComposeAiContent::selectToneById(uint64 id) { + for (auto i = 0; i != int(_tones.size()); ++i) { + const auto &tone = _tones[i]; + if (!tone.isDefault && tone.id == id) { + const auto wasNoSelection = (_styleIndex < 0); + _styleIndex = i; + updateTitles(); + if (_styles) { + _styles->setActive(_styleIndex); + } + if (_mode == ComposeAiMode::Style) { + request(); + if (wasNoSelection && _styleSelected) { + _styleSelected(); + } + } + return; + } + } +} + void ComposeAiContent::start() { updatePinnedTabs(anim::type::instant); updateTitles(); @@ -1629,7 +1656,9 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { EditAiToneBox, session, toneCopy, - nullptr)); + crl::guard(content, [=](Data::AiComposeTone tone) { + content->selectToneById(tone.id); + }))); }, &st::menuIconEdit); (*contextMenu)->addAction( diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp index fc4cc72326..d0d6b0ce79 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -20,9 +20,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/abstract_button.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/checkbox.h" #include "ui/widgets/fields/input_field.h" @@ -39,6 +44,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL 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, @@ -121,7 +155,7 @@ void ChooseToneIconBox( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } -void AddIconPreview( +not_null AddIconPreview( not_null container, not_null session, rpl::producer emojiIdValue, @@ -262,6 +296,8 @@ void AddIconPreview( emojiIdChosen(id); }))); }); + + return button; } void SetupToneBox( @@ -284,7 +320,7 @@ void SetupToneBox( const auto emojiId = container->lifetime().make_state< rpl::variable>(initialEmojiId); - AddIconPreview( + const auto iconButton = AddIconPreview( container, session, emojiId->value(), @@ -441,18 +477,38 @@ void SetupToneBox( name->setFocusFast(); }); + const auto warning = box->lifetime().make_state(); const auto save = [=] { const auto nameText = name->getLastText().trimmed(); const auto promptText = prompt->getLastText().trimmed(); - if (nameText.isEmpty() || promptText.isEmpty()) { - if (nameText.isEmpty()) { - name->showError(); - } - if (promptText.isEmpty()) { - prompt->showError(); - } + 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, @@ -469,7 +525,7 @@ void SetupToneBox( void CreateAiToneBox( not_null box, not_null session, - Fn saved) { + Fn saved) { SetupToneBox( box, session, @@ -488,10 +544,12 @@ void CreateAiToneBox( prompt, emojiId, displayAuthor, - [=](Data::AiComposeTone) { + [=](Data::AiComposeTone tone) { + const auto show = box->uiShow(); box->closeBox(); + ShowToneToast(show, session, tone, true); if (saved) { - saved(); + saved(tone); } }); }); @@ -501,7 +559,7 @@ void EditAiToneBox( not_null box, not_null session, const Data::AiComposeTone &tone, - Fn saved) { + Fn saved) { const auto toneId = tone.id; const auto toneAccessHash = tone.accessHash; SetupToneBox( @@ -526,10 +584,12 @@ void EditAiToneBox( prompt, std::make_optional(emojiId), std::make_optional(displayAuthor), - [=](Data::AiComposeTone) { + [=](Data::AiComposeTone updated) { + const auto show = box->uiShow(); box->closeBox(); + ShowToneToast(show, session, updated, false); if (saved) { - saved(); + saved(updated); } }); }); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h index cfcbf1647e..01730aa33e 100644 --- a/Telegram/SourceFiles/boxes/create_ai_tone_box.h +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -22,10 +22,10 @@ class GenericBox; void CreateAiToneBox( not_null box, not_null session, - Fn saved = nullptr); + Fn saved = nullptr); void EditAiToneBox( not_null box, not_null session, const Data::AiComposeTone &tone, - Fn saved = nullptr); + Fn saved = nullptr); diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp index 438a6870e8..6b3b4dc616 100644 --- a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -91,9 +91,6 @@ void AiComposeTones::create( if (displayAuthor) { flags |= Flag::f_display_author; } - if (emojiId) { - flags |= Flag::f_emoji_id; - } _session->api().request(MTPaicompose_CreateTone( MTP_flags(flags), MTP_long(emojiId), diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index 9a832c36f3..505d6bb74a 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -65,6 +65,7 @@ struct FileReferenceAccumulator { push(data.vicons()); }, [&](const MTPDwebPageAttributeStarGiftAuction &data) { push(data.vgift()); + }, [](const MTPDwebPageAttributeAiComposeTone &) { }); } void push(const MTPStarGift &data) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 96eadb153d..ebd2db5b37 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4012,6 +4012,8 @@ void Session::webpageApplyFields( return (DocumentData*)nullptr; }, [](const MTPDwebPageAttributeStarGiftAuction &) { return (DocumentData*)nullptr; + }, [](const MTPDwebPageAttributeAiComposeTone &) { + return (DocumentData*)nullptr; }); if (result) { return result; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 887525bf18..5334c9166d 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1324,6 +1324,7 @@ webPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags webPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute; webPageAttributeStarGiftCollection#31cad303 icons:Vector = WebPageAttribute; webPageAttributeStarGiftAuction#1c641c2 gift:StarGift end_date:int = WebPageAttribute; +webPageAttributeAiComposeTone#7781fe18 emoji_id:long = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; @@ -2171,6 +2172,8 @@ aicompose.tones#6c9d0efe hash:long tones:Vector users:Vector = bots.AccessSettings; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2768,6 +2771,8 @@ bots.createBot#e5b17f2b flags:# via_deeplink:flags.0?true name:string username:s bots.exportBotToken#bd0d99eb bot:InputUser revoke:Bool = bots.ExportedBotToken; bots.requestWebViewButton#31a2a35e user_id:InputUser button:KeyboardButton = bots.RequestedButton; bots.getRequestedWebViewButton#bf25b7f3 bot:InputUser webapp_req_id:string = KeyboardButton; +bots.getAccessSettings#213853a3 bot:InputUser = bots.AccessSettings; +bots.editAccessSettings#31813cd8 flags:# restricted:flags.0?true bot:InputUser add_users:flags.1?Vector = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2970,7 +2975,7 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -aicompose.createTone#70f76f8 flags:# display_author:flags.0?true emoji_id:flags.1?long title:string prompt:string = AiComposeTone; +aicompose.createTone#4aa83913 flags:# display_author:flags.0?true emoji_id:long title:string prompt:string = AiComposeTone; aicompose.updateTone#903bcf59 flags:# tone:InputAiComposeTone display_author:flags.0?Bool emoji_id:flags.1?long title:flags.2?string prompt:flags.3?string = AiComposeTone; aicompose.saveTone#1782cbb1 tone:InputAiComposeTone unsave:Bool = Bool; aicompose.deleteTone#dd39316a tone:InputAiComposeTone = Bool; diff --git a/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp new file mode 100644 index 0000000000..dd24b43d56 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.cpp @@ -0,0 +1,57 @@ +/* +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 "ui/controls/custom_emoji_toast_icon.h" + +#include "data/stickers/data_custom_emoji.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "ui/text/text_custom_emoji.h" +#include "ui/rp_widget.h" + +#include "styles/style_widgets.h" + +#include + +namespace Ui { + +object_ptr MakeCustomEmojiToastIcon( + not_null session, + DocumentId emojiId, + QSize size) { + auto result = object_ptr((QWidget*)nullptr); + const auto raw = result.data(); + raw->resize(size); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + + struct State { + std::unique_ptr emoji; + }; + const auto state = raw->lifetime().make_state(); + state->emoji = session->data().customEmojiManager().create( + emojiId, + [=] { raw->update(); }, + Data::CustomEmojiManager::SizeTag::Large); + + raw->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(raw); + const auto esize = Emoji::GetCustomSizeLarge(); + const auto position = QPoint( + (raw->width() - esize) / 2, + (raw->height() - esize) / 2); + state->emoji->paint(p, { + .textColor = st::toastFg->c, + .now = crl::now(), + .position = position, + }); + }, raw->lifetime()); + + return result; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h new file mode 100644 index 0000000000..844f1937e3 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/custom_emoji_toast_icon.h @@ -0,0 +1,27 @@ +/* +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 +*/ +#pragma once + +#include "base/object_ptr.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Ui { + +[[nodiscard]] object_ptr MakeCustomEmojiToastIcon( + not_null session, + DocumentId emojiId, + QSize size); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/warning_tooltip.cpp b/Telegram/SourceFiles/ui/controls/warning_tooltip.cpp new file mode 100644 index 0000000000..0e98199f70 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/warning_tooltip.cpp @@ -0,0 +1,112 @@ +/* +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 "ui/controls/warning_tooltip.h" + +#include "base/timer_rpl.h" +#include "ui/effects/animation_value.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/ui_utility.h" + +#include "styles/style_layers.h" +#include "styles/style_widgets.h" + +namespace Ui { +namespace { + +constexpr auto kDefaultWarningTooltipDuration = crl::time(2000); + +} // namespace + +WarningTooltip::WarningTooltip() = default; + +WarningTooltip::~WarningTooltip() { + hide(anim::type::instant); +} + +void WarningTooltip::show(Args &&args) { + const auto duration = args.duration + ? args.duration + : kDefaultWarningTooltipDuration; + const auto maxWidth = args.maxWidth + ? args.maxWidth + : st::boxWideWidth; + const auto &tooltipSt = args.st + ? *args.st + : st::defaultImportantTooltip; + const auto &labelSt = args.labelSt + ? *args.labelSt + : st::defaultImportantTooltipLabel; + + const auto parent = args.parent.get(); + const auto target = args.target; + const auto side = args.side; + auto countPosition = std::move(args.countPosition); + const auto tooltip = CreateChild( + parent, + MakeNiceTooltipLabel( + parent, + std::move(args.text), + maxWidth, + labelSt), + tooltipSt); + tooltip->toggleFast(false); + + const auto update = [=] { + tooltip->pointAt( + MapFrom(parent, target, target->rect()), + side, + countPosition); + }; + parent->widthValue( + ) | rpl::on_next(update, tooltip->lifetime()); + update(); + tooltip->toggleAnimated(true); + + tooltip->shownValue( + ) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::on_next([=] { + crl::on_main(tooltip, [=] { + if (tooltip->isHidden()) { + delete tooltip; + } + }); + }, tooltip->lifetime()); + + base::timer_once( + duration + ) | rpl::on_next([=] { + tooltip->toggleAnimated(false); + }, tooltip->lifetime()); + + if (_current && _current->tooltip) { + _current->tooltip->toggleAnimated(false); + } + auto entry = std::make_unique(); + entry->tooltip = tooltip; + _current = std::move(entry); +} + +void WarningTooltip::hide(anim::type animated) { + if (!_current) { + return; + } + const auto raw = _current->tooltip.data(); + _current.reset(); + if (!raw) { + return; + } + if (animated == anim::type::instant) { + delete raw; + } else { + raw->toggleAnimated(false); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/warning_tooltip.h b/Telegram/SourceFiles/ui/controls/warning_tooltip.h new file mode 100644 index 0000000000..1787dcd9ff --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/warning_tooltip.h @@ -0,0 +1,58 @@ +/* +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 +*/ +#pragma once + +#include "base/object_ptr.h" +#include "ui/rect_part.h" +#include "ui/text/text_entity.h" + +#include + +namespace style { +struct ImportantTooltip; +struct FlatLabel; +} // namespace style + +namespace anim { +enum class type : uchar; +} // namespace anim + +namespace Ui { + +class ImportantTooltip; +class RpWidget; + +class WarningTooltip final { +public: + struct Args { + not_null parent; + not_null target; + rpl::producer text; + RectParts side = RectPart::Top; + int maxWidth = 0; + Fn countPosition; + crl::time duration = 0; + const style::ImportantTooltip *st = nullptr; + const style::FlatLabel *labelSt = nullptr; + }; + + WarningTooltip(); + ~WarningTooltip(); + + void show(Args &&args); + void hide(anim::type animated); + +private: + struct Entry { + QPointer tooltip; + }; + std::unique_ptr _current; + +}; + +} // namespace Ui