diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1c7484a603..fdde7123e2 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -386,6 +386,8 @@ PRIVATE boxes/transfer_gift_box.h boxes/compose_ai_box.cpp boxes/compose_ai_box.h + boxes/create_ai_tone_box.cpp + boxes/create_ai_tone_box.h boxes/translate_box.cpp boxes/translate_box.h boxes/url_auth_box.cpp @@ -611,6 +613,8 @@ PRIVATE data/data_abstract_sparse_ids.h data/data_abstract_structure.cpp data/data_abstract_structure.h + data/data_ai_compose_tones.cpp + data/data_ai_compose_tones.h data/data_audio_msg_id.cpp data/data_audio_msg_id.h data/data_auto_download.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cd42bc0994..9971741fdb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7919,6 +7919,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_select_style" = "Select Style"; "lng_ai_compose_apply_style" = "Apply Style"; "lng_ai_compose_style_tooltip" = "Choose Style"; +"lng_ai_compose_add_style" = "Add Style"; +"lng_ai_compose_create_tone_title" = "New Style"; +"lng_ai_compose_edit_tone_title" = "Edit Style"; +"lng_ai_compose_tone_name" = "Name"; +"lng_ai_compose_tone_prompt" = "Prompt"; +"lng_ai_compose_tone_save" = "Save"; +"lng_ai_compose_tone_edit" = "Edit"; +"lng_ai_compose_tone_share" = "Share"; +"lng_ai_compose_tone_delete" = "Delete"; +"lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style?"; +"lng_ai_compose_tone_save_sure" = "Do you want to save the style {title}?"; +"lng_ai_compose_tone_saved" = "Style saved."; +"lng_ai_compose_tone_link_copied" = "Style link copied."; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/SourceFiles/api/api_compose_with_ai.cpp b/Telegram/SourceFiles/api/api_compose_with_ai.cpp index dfebd67e5f..a6a474e762 100644 --- a/Telegram/SourceFiles/api/api_compose_with_ai.cpp +++ b/Telegram/SourceFiles/api/api_compose_with_ai.cpp @@ -40,8 +40,8 @@ mtpRequestId ComposeWithAi::request( if (!request.translateToLang.isEmpty()) { flags |= Flag::f_translate_to_lang; } - if (!request.changeTone.isEmpty()) { - flags |= Flag::f_change_tone; + if (request.tone) { + flags |= Flag::f_tone; } if (request.emojify) { flags |= Flag::f_emojify; @@ -53,9 +53,14 @@ mtpRequestId ComposeWithAi::request( request.translateToLang.isEmpty() ? MTPstring() : MTP_string(request.translateToLang), - request.changeTone.isEmpty() - ? MTPstring() - : MTP_string(request.changeTone) + request.tone + ? (request.tone->id + ? MTP_inputAiComposeToneID( + MTP_long(request.tone->id), + MTP_long(request.tone->accessHash)) + : MTP_inputAiComposeToneDefault( + MTP_string(request.tone->defaultTone))) + : MTPInputAiComposeTone() )).done([=, done = std::move(done)]( const MTPmessages_ComposedMessageWithAI &result) mutable { const auto &data = result.data(); diff --git a/Telegram/SourceFiles/api/api_compose_with_ai.h b/Telegram/SourceFiles/api/api_compose_with_ai.h index 9a87aa0542..6690ce3d8c 100644 --- a/Telegram/SourceFiles/api/api_compose_with_ai.h +++ b/Telegram/SourceFiles/api/api_compose_with_ai.h @@ -23,12 +23,25 @@ namespace Api { class ComposeWithAi final { public: + struct ToneRef { + QString defaultTone; + uint64 id = 0; + uint64 accessHash = 0; + }; + struct Request { TextWithEntities text; QString translateToLang; - QString changeTone; + std::optional tone; bool proofread = false; bool emojify = false; + + void setDefaultTone(const QString &type) { + tone = ToneRef{ .defaultTone = type }; + } + void setCustomTone(uint64 id, uint64 accessHash) { + tone = ToneRef{ .id = id, .accessHash = accessHash }; + } }; struct DiffEntity { diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index f292b623aa..95837ee07d 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" +#include "data/data_ai_compose_tones.h" #include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -2779,6 +2780,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().api().ringtones().applyUpdate(); } break; + case mtpc_updateAiComposeTones: { + session().data().aiComposeTones().applyUpdate(); + } break; + case mtpc_updateTranscribedAudio: { const auto &data = update.c_updateTranscribedAudio(); _session->api().transcribes().apply(data); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 5a1c8cbbd4..79f00ea1d0 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1246,6 +1246,9 @@ aiComposeBadge: RoundButton(customEmojiTextBadge) { } aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px); +aiComposeAddStyleIcon: icon {{ "menu/edit_stars_add", aiComposeButtonFg }}; +aiComposeAddStyleIconOver: icon {{ "menu/edit_stars_add", aiComposeButtonFgActive }}; + aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 0d51c9d322..44c9ea1cbb 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_compose_with_ai.h" #include "apiwrap.h" +#include "boxes/create_ai_tone_box.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" @@ -16,10 +17,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "core/core_settings.h" #include "core/ui_integration.h" +#include "data/data_ai_compose_tones.h" #include "data/data_document.h" +#include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" -#include "data/data_session.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" #include "main/main_app_config.h" @@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/platform/platform_language.h" #include "ui/boxes/about_cocoon_box.h" #include "ui/boxes/choose_language_box.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/chat_style.h" #include "ui/controls/labeled_emoji_tabs.h" #include "ui/controls/send_button.h" @@ -46,16 +49,20 @@ 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/popup_menu.h" #include "ui/widgets/tooltip.h" #include "styles/style_basic.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_widgets.h" #include #include +#include + namespace HistoryView::Controls { namespace { @@ -232,6 +239,27 @@ enum class CardState { return result; } +[[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor( + const Data::AiComposeTone &tone) { + return { + .id = tone.isDefault ? tone.defaultType : QString::number(tone.id), + .label = tone.title, + .customEmojiData = tone.emojiId + ? Data::SerializeCustomEmojiId(tone.emojiId) + : QString(), + }; +} + +[[nodiscard]] std::vector ResolveStyleDescriptors( + const std::vector &tones) { + auto result = std::vector(); + result.reserve(tones.size()); + for (const auto &tone : tones) { + result.push_back(ResolveStyleDescriptor(tone)); + } + return result; +} + [[nodiscard]] std::vector ResolveTranslateStyleDescriptors( not_null session, const std::vector &styles) { @@ -249,6 +277,17 @@ enum class CardState { return result; } +[[nodiscard]] auto WithAddStyleTab(std::vector tabs) +-> std::vector { + tabs.push_back({ + .id = u"_add_style"_q, + .label = tr::lng_ai_compose_add_style(tr::now), + .icon = &st::aiComposeAddStyleIcon, + .iconActive = &st::aiComposeAddStyleIconOver, + }); + return tabs; +} + [[nodiscard]] TextWithEntities LoadingTitleSparkle( not_null session) { const auto sparkles = ChatHelpers::GenerateLocalTgsSticker( @@ -364,6 +403,7 @@ public: [[nodiscard]] bool hasResult() const; [[nodiscard]] const TextWithEntities &result() const; [[nodiscard]] const std::vector &stylesData() const; + [[nodiscard]] const std::vector &tones() const; void setReadyChangedCallback(Fn callback); void setLoadingChangedCallback(Fn callback); void setPremiumFloodCallback(Fn callback); @@ -400,6 +440,7 @@ private: const TextWithEntities _original; const LanguageId _detectedFrom; LanguageId _to; + const std::vector _tones; const std::vector _stylesData; const std::vector _translateStylesData; QPointer _tabs; @@ -934,8 +975,8 @@ ComposeAiContent::ComposeAiContent( , _original(std::move(args.text)) , _detectedFrom(Platform::Language::Recognize(_original.text)) , _to(DefaultAiTranslateTo(_detectedFrom)) -, _stylesData(ResolveStyleDescriptors( - _session->appConfig().aiComposeStyles())) +, _tones(_session->data().aiComposeTones().list()) +, _stylesData(ResolveStyleDescriptors(_tones)) , _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData)) , _preview( Ui::CreateChild( @@ -971,6 +1012,10 @@ const std::vector &ComposeAiContent::stylesData() const { return _stylesData; } +const std::vector &ComposeAiContent::tones() const { + return _tones; +} + void ComposeAiContent::setReadyChangedCallback(Fn callback) { _readyChanged = std::move(callback); } @@ -994,7 +1039,7 @@ void ComposeAiContent::setStyleTabs( _stylesWrap->setDuration(0); _styles = stylesWrap->entity(); _styles->setChangedCallback([=](int index) { - if (index >= 0 && index < int(_stylesData.size())) { + if (index >= 0 && index < int(_tones.size())) { const auto wasNoSelection = (_styleIndex < 0); _styleIndex = index; updateTitles(); @@ -1004,6 +1049,9 @@ void ComposeAiContent::setStyleTabs( _styleSelected(); } } + } else if (index == int(_tones.size())) { + _styles->setActive(_styleIndex); + _box->uiShow()->show(Box(CreateAiToneBox, _session, nullptr)); } }); _styles->setActive(_styleIndex); @@ -1173,13 +1221,21 @@ void ComposeAiContent::request() { .emojify = (_mode != ComposeAiMode::Fix) && _emojify, }; switch (_mode) { - case ComposeAiMode::Translate: + case ComposeAiMode::Translate: { request.translateToLang = _to.twoLetterCode(); - request.changeTone = currentTranslateStyle(); - break; + const auto style = currentTranslateStyle(); + if (!style.isEmpty()) { + request.setDefaultTone(style); + } + } break; case ComposeAiMode::Style: - if (_styleIndex >= 0) { - request.changeTone = _stylesData[_styleIndex].id; + if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { + const auto &tone = _tones[_styleIndex]; + if (tone.isDefault) { + request.setDefaultTone(tone.defaultType); + } else { + request.setCustomTone(tone.id, tone.accessHash); + } } break; case ComposeAiMode::Fix: @@ -1440,11 +1496,63 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { pinnedToTop, object_ptr( pinnedToTop, - content->stylesData(), + WithAddStyleTab(content->stylesData()), std::move(emojiFactory)), tabsSkip), st::aiComposeContentMargin); stylesWrap->hide(anim::type::instant); + + const auto contextMenu = box->lifetime().make_state< + base::unique_qptr>(); + stylesWrap->entity()->setContextMenuCallback([=](int index, QPoint globalPos) { + const auto &tones = content->tones(); + if (index < 0 || index >= int(tones.size())) { + return; + } + const auto &tone = tones[index]; + if (!tone.creator) { + return; + } + *contextMenu = base::make_unique_q( + stylesWrap->entity(), + st::popupMenuWithIcons); + const auto toneCopy = tone; + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_edit(tr::now), + [=] { + box->uiShow()->show(Box( + EditAiToneBox, + session, + toneCopy, + nullptr)); + }, + &st::menuIconEdit); + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_share(tr::now), + [=] { + QGuiApplication::clipboard()->setText( + session->createInternalLinkFull( + "aistyle/" + toneCopy.slug)); + box->showToast( + tr::lng_ai_compose_tone_link_copied(tr::now)); + }, + &st::menuIconShare); + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_delete(tr::now), + [=] { + box->uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_delete_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().remove(toneCopy); + }, + .confirmText = tr::lng_ai_compose_tone_delete(), + })); + }, + &st::menuIconDelete); + (*contextMenu)->popup(globalPos); + }); + content->setModeTabs(tabs); content->setStyleTabs(stylesWrap); diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp new file mode 100644 index 0000000000..55c0462da1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -0,0 +1,129 @@ +/* +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 "data/data_ai_compose_tones.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/layers/generic_box.h" +#include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" + +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +void SetupToneBox( + not_null box, + not_null session, + const QString &initialName, + const QString &initialPrompt, + rpl::producer title, + Fn submit) { + box->setTitle(std::move(title)); + + const auto name = box->addRow(object_ptr( + box, + st::defaultInputField, + tr::lng_ai_compose_tone_name(), + initialName)); + + Ui::AddSkip(box->verticalLayout()); + + const auto promptSt = box->lifetime().make_state( + st::newGroupDescription); + const auto prompt = box->addRow(object_ptr( + box, + *promptSt, + Ui::InputField::Mode::MultiLine, + tr::lng_ai_compose_tone_prompt(), + initialPrompt)); + + box->setFocusCallback([=] { + name->setFocusFast(); + }); + + 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(); + } + return; + } + submit(nameText, promptText); + }; + + box->addButton(tr::lng_ai_compose_tone_save(), save); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +} // namespace + +void CreateAiToneBox( + not_null box, + not_null session, + Fn saved) { + SetupToneBox( + box, + session, + QString(), + QString(), + tr::lng_ai_compose_create_tone_title(), + [=](const QString &name, const QString &prompt) { + session->data().aiComposeTones().create( + name, + prompt, + 0, + false, + [=](Data::AiComposeTone) { + box->closeBox(); + if (saved) { + saved(); + } + }); + }); +} + +void EditAiToneBox( + not_null box, + not_null session, + const Data::AiComposeTone &tone, + Fn saved) { + const auto toneId = tone.id; + const auto toneAccessHash = tone.accessHash; + SetupToneBox( + box, + session, + tone.title, + tone.prompt, + tr::lng_ai_compose_edit_tone_title(), + [=](const QString &name, const QString &prompt) { + auto toneCopy = Data::AiComposeTone(); + toneCopy.id = toneId; + toneCopy.accessHash = toneAccessHash; + session->data().aiComposeTones().update( + toneCopy, + name, + prompt, + std::nullopt, + std::nullopt, + [=](Data::AiComposeTone) { + box->closeBox(); + if (saved) { + saved(); + } + }); + }); +} diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h new file mode 100644 index 0000000000..cfcbf1647e --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -0,0 +1,31 @@ +/* +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 + +namespace Data { +struct AiComposeTone; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +void CreateAiToneBox( + not_null box, + not_null session, + Fn saved = nullptr); + +void EditAiToneBox( + not_null box, + not_null session, + const Data::AiComposeTone &tone, + Fn saved = nullptr); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 1ae2a02ee3..62c3e924fc 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "data/components/credits.h" +#include "data/data_ai_compose_tones.h" #include "data/data_birthday.h" #include "data/data_channel.h" #include "data/data_document.h" @@ -294,6 +295,48 @@ bool ShowTheme( return true; } +bool ShowAiStyle( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto slug = match->captured(1); + Core::App().hideMediaView(); + const auto weak = base::make_weak(controller); + auto &tones = controller->session().data().aiComposeTones(); + tones.resolve(slug, [=](Data::AiComposeTone tone) { + const auto strong = weak.get(); + if (!strong) { + return; + } + strong->window().show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_save_sure( + tr::now, + lt_title, + tone.title), + .confirmed = [=](Fn &&close) { + close(); + strong->session().data().aiComposeTones().save( + tone, + false); + strong->window().showToast( + tr::lng_ai_compose_tone_saved(tr::now)); + }, + .confirmText = tr::lng_ai_compose_tone_save(), + })); + }, [=](const MTP::Error &error) { + const auto strong = weak.get(); + if (!strong) { + return; + } + strong->window().showToast(tr::lng_ai_compose_error(tr::now)); + }); + controller->window().activate(); + return true; +} + void ShowLanguagesBox(Window::SessionController *controller) { static auto Guard = base::binary_guard(); Guard = LanguageBox::Show(controller); @@ -1661,6 +1704,10 @@ const std::vector &LocalUrlHandlers() { u"^addtheme/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, ShowTheme }, + { + u"^aistyle/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, + ShowAiStyle + }, { u"^setlanguage/?(\\?lang=([a-zA-Z0-9\\.\\_\\-]+))?(&|$)"_q, SetLanguage @@ -1878,6 +1925,8 @@ QString TryConvertUrlToLocal(QString url) { return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2)); } else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://addtheme?slug="_q + url_encode(themeMatch->captured(1)); + } else if (const auto aiStyleMatch = regex_match(u"^aistyle/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { + return u"tg://aistyle?slug="_q + url_encode(aiStyleMatch->captured(1)); } else if (const auto languageMatch = regex_match(u"^setlanguage/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://setlanguage?lang="_q + url_encode(languageMatch->captured(1)); } else if (const auto shareUrlMatch = regex_match(u"^share/url/?\\?(.+)$"_q, query, matchOptions)) { diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.cpp b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp new file mode 100644 index 0000000000..1c2b253d5e --- /dev/null +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.cpp @@ -0,0 +1,225 @@ +/* +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 "data/data_ai_compose_tones.h" + +#include "main/main_session.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRefreshInterval = 3600 * crl::time(1000); + +} // namespace + +AiComposeTones::AiComposeTones(not_null session) +: _session(session) +, _refreshTimer([=] { refresh(); }) { + refresh(); + _refreshTimer.callEach(kRefreshInterval); +} + +void AiComposeTones::refresh() { + if (_refreshRequestId) { + return; + } + _refreshRequestId = _session->api().request(MTPaicompose_GetTones( + MTP_long(_hash) + )).done([=](const MTPaicompose_Tones &result) { + _refreshRequestId = 0; + result.match([&](const MTPDaicompose_tones &data) { + _hash = data.vhash().v; + parseTones(data.vtones().v); + _updates.fire({}); + }, [](const MTPDaicompose_tonesNotModified &) { + }); + }).fail([=] { + _refreshRequestId = 0; + }).send(); +} + +void AiComposeTones::parseTones(const QVector &list) { + _list.clear(); + _list.reserve(list.size()); + for (const auto &tone : list) { + _list.push_back(parseTone(tone)); + } +} + +AiComposeTone AiComposeTones::parseTone( + const MTPAiComposeTone &tone) const { + return tone.match([&](const MTPDaiComposeTone &data) { + return AiComposeTone{ + .id = data.vid().v, + .accessHash = data.vaccess_hash().v, + .slug = qs(data.vslug()), + .title = qs(data.vtitle()), + .emojiId = data.vemoji_id().value_or_empty(), + .prompt = qs(data.vprompt()), + .installsCount = data.vinstalls_count().value_or_empty(), + .authorId = data.vauthor_id() + ? UserId(data.vauthor_id()->v) + : UserId(0), + .creator = data.is_creator(), + }; + }, [&](const MTPDaiComposeToneDefault &data) { + return AiComposeTone{ + .title = qs(data.vtitle()), + .emojiId = data.vemoji_id().v, + .isDefault = true, + .defaultType = qs(data.vtone()), + }; + }); +} + +void AiComposeTones::create( + const QString &title, + const QString &prompt, + DocumentId emojiId, + bool displayAuthor, + Fn done, + Fn fail) { + using Flag = MTPaicompose_CreateTone::Flag; + auto flags = MTPaicompose_CreateTone::Flags(0); + 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), + MTP_string(title), + MTP_string(prompt) + )).done([=](const MTPAiComposeTone &result) { + auto parsed = parseTone(result); + if (done) { + done(parsed); + } + refresh(); + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + +void AiComposeTones::update( + const AiComposeTone &tone, + std::optional title, + std::optional prompt, + std::optional emojiId, + std::optional displayAuthor, + Fn done, + Fn fail) { + using Flag = MTPaicompose_UpdateTone::Flag; + auto flags = MTPaicompose_UpdateTone::Flags(0); + if (displayAuthor) { + flags |= Flag::f_display_author; + } + if (emojiId) { + flags |= Flag::f_emoji_id; + } + if (title) { + flags |= Flag::f_title; + } + if (prompt) { + flags |= Flag::f_prompt; + } + _session->api().request(MTPaicompose_UpdateTone( + MTP_flags(flags), + toneToMTP(tone), + displayAuthor + ? (*displayAuthor ? MTP_boolTrue() : MTP_boolFalse()) + : MTPBool(), + MTP_long(emojiId.value_or(0)), + MTP_string(title.value_or(QString())), + MTP_string(prompt.value_or(QString())) + )).done([=](const MTPAiComposeTone &result) { + auto parsed = parseTone(result); + if (done) { + done(parsed); + } + refresh(); + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + +void AiComposeTones::save( + const AiComposeTone &tone, + bool unsave, + Fn done) { + _session->api().request(MTPaicompose_SaveTone( + toneToMTP(tone), + unsave ? MTP_boolTrue() : MTP_boolFalse() + )).done([=] { + if (done) { + done(); + } + refresh(); + }).fail([=] { + }).send(); +} + +void AiComposeTones::remove( + const AiComposeTone &tone, + Fn done) { + _session->api().request(MTPaicompose_DeleteTone( + toneToMTP(tone) + )).done([=] { + if (done) { + done(); + } + refresh(); + }).fail([=] { + }).send(); +} + +void AiComposeTones::resolve( + const QString &slug, + Fn done, + Fn fail) { + _session->api().request(MTPaicompose_GetTone( + MTP_inputAiComposeToneSlug(MTP_string(slug)) + )).done([=](const MTPAiComposeTone &result) { + if (done) { + done(parseTone(result)); + } + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error); + } + }).send(); +} + +void AiComposeTones::applyUpdate() { + refresh(); +} + +MTPInputAiComposeTone AiComposeTones::toneToMTP( + const AiComposeTone &tone) const { + return tone.isDefault + ? MTP_inputAiComposeToneDefault(MTP_string(tone.defaultType)) + : MTP_inputAiComposeToneID( + MTP_long(tone.id), + MTP_long(tone.accessHash)); +} + +const std::vector &AiComposeTones::list() const { + return _list; +} + +rpl::producer<> AiComposeTones::updated() const { + return _updates.events(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_ai_compose_tones.h b/Telegram/SourceFiles/data/data_ai_compose_tones.h new file mode 100644 index 0000000000..c17e8f2738 --- /dev/null +++ b/Telegram/SourceFiles/data/data_ai_compose_tones.h @@ -0,0 +1,90 @@ +/* +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/timer.h" + +namespace Main { +class Session; +} // namespace Main + +namespace MTP { +class Error; +} // namespace MTP + +namespace Data { + +struct AiComposeTone { + uint64 id = 0; + uint64 accessHash = 0; + QString slug; + QString title; + DocumentId emojiId = 0; + QString prompt; + int installsCount = 0; + UserId authorId = 0; + bool creator = false; + bool isDefault = false; + QString defaultType; +}; + +class AiComposeTones final { +public: + explicit AiComposeTones(not_null session); + + void refresh(); + [[nodiscard]] const std::vector &list() const; + [[nodiscard]] rpl::producer<> updated() const; + + void create( + const QString &title, + const QString &prompt, + DocumentId emojiId, + bool displayAuthor, + Fn done, + Fn fail = nullptr); + void update( + const AiComposeTone &tone, + std::optional title, + std::optional prompt, + std::optional emojiId, + std::optional displayAuthor, + Fn done, + Fn fail = nullptr); + void save( + const AiComposeTone &tone, + bool unsave, + Fn done = nullptr); + void remove( + const AiComposeTone &tone, + Fn done = nullptr); + void resolve( + const QString &slug, + Fn done, + Fn fail = nullptr); + + void applyUpdate(); + + [[nodiscard]] MTPInputAiComposeTone toneToMTP( + const AiComposeTone &tone) const; + +private: + void parseTones(const QVector &list); + [[nodiscard]] AiComposeTone parseTone( + const MTPAiComposeTone &tone) const; + + const not_null _session; + uint64 _hash = 0; + mtpRequestId _refreshRequestId = 0; + std::vector _list; + rpl::event_stream<> _updates; + base::Timer _refreshTimer; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 10d1340d6d..b23bd4234d 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -192,7 +192,7 @@ bool PollData::applyResults(const MTPPollResults &results) { auto changed = (newTotalVoters != totalVoters); const auto setCanViewStats = [&](bool value) { const auto previous = (_flags & Flag::CanViewStats); - if (previous == value) { + if (bool(previous) == value) { return; } if (value) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index c5313fb154..96eadb153d 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/sponsored_messages.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" +#include "data/data_ai_compose_tones.h" #include "data/data_bot_app.h" #include "data/data_changes.h" #include "data/data_group_call.h" @@ -242,6 +243,7 @@ Session::Session(not_null session) , _pollsClosingTimer([=] { checkPollsClosings(); }) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _groups(this) +, _aiComposeTones(std::make_unique(session)) , _chatsFilters(std::make_unique(this)) , _cloudThemes(std::make_unique(session)) , _sendActionManager(std::make_unique()) diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index e66c1b81d3..f41afac491 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -59,6 +59,7 @@ class SendActionManager; class Reactions; class EmojiStatuses; class ForumIcons; +class AiComposeTones; class ChatFilters; class CloudThemes; class Streaming; @@ -174,6 +175,9 @@ public: [[nodiscard]] const Groups &groups() const { return _groups; } + [[nodiscard]] AiComposeTones &aiComposeTones() const { + return *_aiComposeTones; + } [[nodiscard]] ChatFilters &chatsFilters() const { return *_chatsFilters; } @@ -1325,6 +1329,7 @@ private: mutable base::flat_map> _messagesWithPeer; Groups _groups; + const std::unique_ptr _aiComposeTones; const std::unique_ptr _chatsFilters; const std::unique_ptr _cloudThemes; const std::unique_ptr _sendActionManager; diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp index b4b7b28189..7c670a311d 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/compose_ai_box.h" #include "config.h" #include "core/mime_type.h" +#include "data/data_ai_compose_tones.h" +#include "data/data_session.h" #include "history/view/controls/history_view_compose_ai_button.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -36,7 +38,8 @@ bool HasEnoughLinesForAi( not_null session, not_null field) { if (HideAiButtonOption.value() - || session->appConfig().aiComposeStyles().empty()) { + || (session->appConfig().aiComposeStyles().empty() + && session->data().aiComposeTones().list().empty())) { return false; } const auto &style = field->st().style; diff --git a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp index d60e5a77b6..ef086baa37 100644 --- a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp +++ b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include #include @@ -58,15 +59,18 @@ public: void setSelected(bool selected); void setExtraPadding(int extra); + void setContextMenuCallback(Fn callback); [[nodiscard]] const QString &id() const; protected: void paintEvent(QPaintEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; [[nodiscard]] QImage prepareRippleMask() const override; private: const LabeledEmojiTab _descriptor; std::unique_ptr _custom; + Fn _contextMenuCallback; bool _selected = false; int _extraPadding = 0; @@ -110,7 +114,7 @@ LabeledEmojiTabs::Button::Button( const auto padding = st::aiComposeStyleButtonPadding; const auto labelWidth = st::aiComposeStyleLabelFont->width( _descriptor.label); - const auto emojiWidth = (_custom || _descriptor.emoji) + const auto emojiWidth = (_custom || _descriptor.emoji || _descriptor.icon) ? (Emoji::GetSizeLarge() / style::DevicePixelRatio()) : 0; return padding.left() @@ -180,6 +184,13 @@ void LabeledEmojiTabs::Button::paintEvent(QPaintEvent *e) { Emoji::GetSizeLarge(), left, st::aiComposeStyleEmojiTop); + } else if (_descriptor.icon) { + const auto &icon = _selected + ? *_descriptor.iconActive + : *_descriptor.icon; + icon.paintInCenter( + p, + QRect(0, 0, width(), st::aiComposeStyleLabelTop)); } p.setPen(_selected @@ -205,6 +216,17 @@ QImage LabeledEmojiTabs::Button::prepareRippleMask() const { }); } +void LabeledEmojiTabs::Button::setContextMenuCallback( + Fn callback) { + _contextMenuCallback = std::move(callback); +} + +void LabeledEmojiTabs::Button::contextMenuEvent(QContextMenuEvent *e) { + if (_contextMenuCallback) { + _contextMenuCallback(e->globalPos()); + } +} + LabeledEmojiScrollTabs::DragScroll::DragScroll( not_null parent, not_null scroll, @@ -332,6 +354,18 @@ void LabeledEmojiTabs::setChangedCallback(Fn callback) { _changed = std::move(callback); } +void LabeledEmojiTabs::setContextMenuCallback( + Fn callback) { + _contextMenu = std::move(callback); + for (auto i = 0; i != int(_buttons.size()); ++i) { + _buttons[i]->setContextMenuCallback([=](QPoint globalPos) { + if (_contextMenu) { + _contextMenu(i, globalPos); + } + }); + } +} + void LabeledEmojiTabs::setActive(int index) { if (index < -1 || index >= int(_buttons.size())) { return; @@ -530,6 +564,11 @@ void LabeledEmojiScrollTabs::setChangedCallback(Fn callback) { _inner->setChangedCallback(std::move(callback)); } +void LabeledEmojiScrollTabs::setContextMenuCallback( + Fn callback) { + _inner->setContextMenuCallback(std::move(callback)); +} + void LabeledEmojiScrollTabs::setActive(int index) { _inner->setActive(index); } diff --git a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h index 63dfb9c7ef..08300f5d08 100644 --- a/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h +++ b/Telegram/SourceFiles/ui/controls/labeled_emoji_tabs.h @@ -25,6 +25,8 @@ struct LabeledEmojiTab { QString label; EmojiPtr emoji = nullptr; QString customEmojiData; + const style::icon *icon = nullptr; + const style::icon *iconActive = nullptr; }; class LabeledEmojiScrollTabs; @@ -39,6 +41,7 @@ public: Text::CustomEmojiFactory factory); void setChangedCallback(Fn callback); + void setContextMenuCallback(Fn callback); void setActive(int index); void resizeForOuterWidth(int outerWidth); [[nodiscard]] QString currentId() const; @@ -53,6 +56,7 @@ private: std::vector> _buttons; Fn _changed; + Fn _contextMenu; int _active = -1; rpl::event_stream _requestShown; @@ -67,6 +71,7 @@ public: ~LabeledEmojiScrollTabs(); void setChangedCallback(Fn callback); + void setContextMenuCallback(Fn callback); void setActive(int index); void setPaintOuterCorners(bool paint); void scrollToActive();