From 719466fcac1499fc7c7b571d578cd6ca33a352b9 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 26 Jan 2023 19:36:43 +0400 Subject: [PATCH] Initial chat-translation feature implementation. --- Telegram/CMakeLists.txt | 6 + Telegram/SourceFiles/boxes/language_box.cpp | 27 +- Telegram/SourceFiles/boxes/translate_box.cpp | 46 +-- Telegram/SourceFiles/boxes/translate_box.h | 3 - .../chat_helpers/message_field.cpp | 6 +- Telegram/SourceFiles/core/core_settings.cpp | 117 +++++- Telegram/SourceFiles/core/core_settings.h | 19 +- Telegram/SourceFiles/data/data_changes.h | 81 ++-- Telegram/SourceFiles/data/data_channel.cpp | 1 + Telegram/SourceFiles/data/data_chat.cpp | 1 + .../SourceFiles/data/data_media_types.cpp | 4 + Telegram/SourceFiles/data/data_peer.cpp | 26 ++ Telegram/SourceFiles/data/data_peer.h | 10 + Telegram/SourceFiles/data/data_types.h | 3 + Telegram/SourceFiles/data/data_user.cpp | 1 + Telegram/SourceFiles/history/history.cpp | 46 ++- Telegram/SourceFiles/history/history.h | 11 + .../history/history_inner_widget.cpp | 14 +- .../history/history_inner_widget.h | 2 + Telegram/SourceFiles/history/history_item.cpp | 159 +++++++- Telegram/SourceFiles/history/history_item.h | 16 +- .../history/history_item_components.h | 10 + .../history/history_translation.cpp | 51 +++ .../SourceFiles/history/history_translation.h | 32 ++ .../SourceFiles/history/history_widget.cpp | 56 ++- Telegram/SourceFiles/history/history_widget.h | 5 + .../view/history_view_context_menu.cpp | 4 +- .../history/view/history_view_element.cpp | 14 +- .../history/view/history_view_element.h | 8 + .../history/view/history_view_item_preview.h | 1 + .../view/history_view_replies_section.cpp | 14 +- .../view/history_view_translate_bar.cpp | 197 ++++++++++ .../history/view/history_view_translate_bar.h | 55 +++ .../view/history_view_translate_tracker.cpp | 351 ++++++++++++++++++ .../view/history_view_translate_tracker.h | 76 ++++ .../history/view/media/history_view_media.cpp | 2 +- .../media/view/media_view_overlay_widget.cpp | 2 +- Telegram/SourceFiles/ui/chat/chat.style | 16 + Telegram/lib_spellcheck | 2 +- Telegram/lib_ui | 2 +- 40 files changed, 1356 insertions(+), 141 deletions(-) create mode 100644 Telegram/SourceFiles/history/history_translation.cpp create mode 100644 Telegram/SourceFiles/history/history_translation.h create mode 100644 Telegram/SourceFiles/history/view/history_view_translate_bar.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_translate_bar.h create mode 100644 Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_translate_tracker.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 62c13da7d..e794043f2 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -756,6 +756,10 @@ PRIVATE history/view/history_view_sticker_toast.h history/view/history_view_transcribe_button.cpp history/view/history_view_transcribe_button.h + history/view/history_view_translate_bar.cpp + history/view/history_view_translate_bar.h + history/view/history_view_translate_tracker.cpp + history/view/history_view_translate_tracker.h history/view/history_view_top_bar_widget.cpp history/view/history_view_top_bar_widget.h history/view/history_view_view_button.cpp @@ -782,6 +786,8 @@ PRIVATE history/history_inner_widget.h history/history_location_manager.cpp history/history_location_manager.h + history/history_translation.cpp + history/history_translation.h history/history_unread_things.cpp history/history_unread_things.h history/history_view_highlight_manager.cpp diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index 99f496c7f..98384e5f4 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_instance.h" #include "lang/lang_cloud_manager.h" #include "settings/settings_common.h" +#include "spellcheck/spellcheck_types.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_info.h" @@ -43,6 +44,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace { +namespace { + +[[nodiscard]] std::vector SkipLocalesFromSettings() { + const auto list = Core::App().settings().skipTranslationLanguages(); + return list + | ranges::views::transform(&LanguageId::locale) + | ranges::to_vector; +} + +} // namespace using Language = Lang::Language; using Languages = Lang::CloudManager::Languages; @@ -1138,19 +1149,19 @@ void LanguageBox::prepare() { }), st::settingsButtonNoIcon); - label->fire(Ui::Translate::LocalesFromSettings()); + label->fire(SkipLocalesFromSettings()); translateSkip->setClickedCallback([=] { Ui::BoxShow(this).showBox( - Box(Ui::ChooseLanguageBox, [=](std::vector locales) { + Box(Ui::ChooseLanguageBox, [=](Locales locales) { label->fire_copy(locales); - const auto result = ranges::views::all( + using namespace ranges::views; + Core::App().settings().setSkipTranslationLanguages(all( locales - ) | ranges::views::transform([](const QLocale &l) { - return int(l.language()); - }) | ranges::to_vector; - Core::App().settings().setSkipTranslationForLanguages(result); + ) | transform([](const QLocale &l) { + return LanguageId{ l.language() }; + }) | ranges::to_vector); Core::App().saveSettingsDelayed(); - }, Ui::Translate::LocalesFromSettings()), + }, SkipLocalesFromSettings()), Ui::LayerOption::KeepOther); }); Settings::AddSkip(topContainer); diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index cab4bd81a..7e8f3258c 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -16,9 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "mtproto/sender.h" #include "settings/settings_common.h" -#ifndef TDESKTOP_DISABLE_SPELLCHECK #include "spellcheck/platform/platform_language.h" -#endif #include "ui/effects/loading_element.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" @@ -252,29 +250,6 @@ rpl::producer ShowButton::clicks() const { } // namespace -namespace Translate { - -std::vector LocalesFromSettings() { - const auto langs = Core::App().settings().skipTranslationForLanguages(); - if (langs.empty()) { - return { QLocale(QLocale::English) }; - } - return ranges::views::all( - langs - ) | ranges::view::transform([](int langId) { - const auto lang = QLocale::Language(langId); - return (lang == QLocale::English) - ? QLocale(Lang::LanguageIdOrDefault(Lang::Id())) - : (lang == QLocale::C) - ? QLocale(QLocale::English) - : QLocale(lang); - }) | ranges::to_vector; -} - -} // namespace Translate - -using namespace Translate; - QString LanguageName(const QLocale &locale) { if (locale.language() == QLocale::English && (locale.country() == QLocale::UnitedStates @@ -297,7 +272,7 @@ void TranslateBox( box->setWidth(st::boxWideWidth); box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); const auto container = box->verticalLayout(); - const auto defaultId = LocalesFromSettings().front().name().mid(0, 2); + const auto translateTo = Core::App().settings().translateTo().locale(); const auto api = box->lifetime().make_state( &peer->session().mtp()); @@ -316,7 +291,7 @@ void TranslateBox( msgId = 0; } - using Flag = MTPmessages_translateText::Flag; + using Flag = MTPmessages_TranslateText::Flag; const auto flags = msgId ? (Flag::f_peer | Flag::f_id) : !text.text.isEmpty() @@ -428,7 +403,7 @@ void TranslateBox( : MTP_vector(1, MTP_textWithEntities( MTP_string(text.text), MTP_vector()))), - MTP_string(toLang) + MTP_string(toLang.mid(0, 2)) )).done([=](const MTPmessages_TranslatedText &result) { const auto &data = result.data(); const auto &list = data.vresult().v; @@ -439,8 +414,8 @@ void TranslateBox( showText(tr::lng_translate_box_error(tr::now)); }).send(); }; - send(defaultId); - state->locale.fire(QLocale(defaultId)); + send(translateTo.name()); + state->locale.fire_copy(translateTo); box->addLeftButton(tr::lng_settings_language(), [=] { if (loading->toggled()) { @@ -449,10 +424,10 @@ void TranslateBox( Ui::BoxShow(box).showBox(Box(ChooseLanguageBox, [=]( std::vector locales) { const auto &locale = locales.front(); + send(locale.name()); state->locale.fire_copy(locale); loading->show(anim::type::instant); translated->hide(anim::type::instant); - send(locale.name().mid(0, 2)); }, std::vector())); }); } @@ -574,12 +549,9 @@ bool SkipTranslate(TextWithEntities textWithEntities) { } #ifndef TDESKTOP_DISABLE_SPELLCHECK const auto result = Platform::Language::Recognize(text); - if (result.unknown) { - return false; - } - return ranges::any_of(LocalesFromSettings(), [&](const QLocale &l) { - return result.locale.language() == l.language(); - }); + const auto skip = Core::App().settings().skipTranslationLanguages(); + const auto test = (result == result); + return result.known() && ranges::contains(skip, result); #else return false; #endif diff --git a/Telegram/SourceFiles/boxes/translate_box.h b/Telegram/SourceFiles/boxes/translate_box.h index e417ebab7..760e0c8d5 100644 --- a/Telegram/SourceFiles/boxes/translate_box.h +++ b/Telegram/SourceFiles/boxes/translate_box.h @@ -10,9 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; namespace Ui { -namespace Translate { -[[nodiscard]] std::vector LocalesFromSettings(); -} // namespace Translate class GenericBox; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 354657323..471a0434d 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -223,13 +223,13 @@ void EditLinkBox( QObject::connect(text, &Ui::InputField::tabbed, [=] { url->setFocus(); }); } -TextWithEntities StripSupportHashtag(TextWithEntities &&text) { +TextWithEntities StripSupportHashtag(TextWithEntities text) { static const auto expression = QRegularExpression( u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q, QRegularExpression::CaseInsensitiveOption); const auto match = expression.match(text.text); if (!match.hasMatch()) { - return std::move(text); + return text; } text.text.chop(match.capturedLength()); const auto length = text.text.size(); @@ -246,7 +246,7 @@ TextWithEntities StripSupportHashtag(TextWithEntities &&text) { } ++i; } - return std::move(text); + return text; } } // namespace diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 546aa8763..6790af9b2 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/player/media_player_instance.h" #include "ui/gl/gl_detection.h" #include "calls/group/calls_group_common.h" +#include "spellcheck/spellcheck_types.h" namespace Core { namespace { @@ -91,10 +92,13 @@ Settings::Settings() , _dialogsWidthRatio(DefaultDialogsWidthRatio()) { } +Settings::~Settings() = default; + QByteArray Settings::serialize() const { const auto themesAccentColors = _themesAccentColors.serialize(); const auto windowPosition = Serialize(_windowPosition); const auto proxy = _proxy.serialize(); + const auto skipLanguages = _skipTranslationLanguages.current(); auto recentEmojiPreloadGenerated = std::vector(); if (_recentEmojiPreload.empty()) { @@ -152,7 +156,10 @@ QByteArray Settings::serialize() const { + Serialize::stringSize(_customDeviceModel.current()) + sizeof(qint32) * 4 + (_accountsOrder.size() * sizeof(quint64)) - + sizeof(qint32) * 5; + + sizeof(qint32) * 7 + + (skipLanguages.size() * sizeof(quint64)) + + sizeof(qint32) + + sizeof(quint64); auto result = QByteArray(); result.reserve(size); @@ -270,13 +277,17 @@ QByteArray Settings::serialize() const { << qint32(_translateButtonEnabled ? 1 : 0); stream - << qint32(_skipTranslationForLanguages.size()); - for (const auto &lang : _skipTranslationForLanguages) { - stream << quint64(lang); + << qint32(skipLanguages.size()); + for (const auto &id : skipLanguages) { + stream << quint64(id.value); } stream << qint32(_rememberedDeleteMessageOnlyForYou ? 1 : 0); + + stream + << qint32(_translateChatEnabled.current() ? 1 : 0) + << quint64(QLocale::Language(_translateToRaw.current())); } return result; } @@ -371,9 +382,11 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 suggestAnimatedEmoji = _suggestAnimatedEmoji ? 1 : 0; qint32 cornerReaction = _cornerReaction.current() ? 1 : 0; qint32 legacySkipTranslationForLanguage = _translateButtonEnabled ? 1 : 0; - qint32 skipTranslationForLanguagesCount = 0; - std::vector skipTranslationForLanguages; + qint32 skipTranslationLanguagesCount = 0; + std::vector skipTranslationLanguages; qint32 rememberedDeleteMessageOnlyForYou = _rememberedDeleteMessageOnlyForYou ? 1 : 0; + qint32 translateChatEnabled = _translateChatEnabled.current() ? 1 : 0; + quint64 translateToRaw = _translateToRaw.current(); stream >> themesAccentColors; if (!stream.atEnd()) { @@ -575,17 +588,24 @@ void Settings::addFromSerialized(const QByteArray &serialized) { stream >> legacySkipTranslationForLanguage; } if (!stream.atEnd()) { - stream >> skipTranslationForLanguagesCount; + stream >> skipTranslationLanguagesCount; if (stream.status() == QDataStream::Ok) { - for (auto i = 0; i != skipTranslationForLanguagesCount; ++i) { + for (auto i = 0; i != skipTranslationLanguagesCount; ++i) { quint64 language; stream >> language; - skipTranslationForLanguages.emplace_back(language); + skipTranslationLanguages.push_back({ + QLocale::Language(language) + }); } } stream >> rememberedDeleteMessageOnlyForYou; } + if (!stream.atEnd()) { + stream + >> translateChatEnabled + >> translateToRaw; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -756,18 +776,21 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _suggestAnimatedEmoji = (suggestAnimatedEmoji == 1); _cornerReaction = (cornerReaction == 1); { // Parse the legacy translation setting. - _skipTranslationForLanguages = skipTranslationForLanguages; if (legacySkipTranslationForLanguage == 0) { _translateButtonEnabled = false; } else if (legacySkipTranslationForLanguage == 1) { _translateButtonEnabled = true; } else { _translateButtonEnabled = (legacySkipTranslationForLanguage > 0); - _skipTranslationForLanguages.push_back( - std::abs(legacySkipTranslationForLanguage)); + skipTranslationLanguages.push_back({ + QLocale::Language(std::abs(legacySkipTranslationForLanguage)) + }); } + _skipTranslationLanguages = std::move(skipTranslationLanguages); } _rememberedDeleteMessageOnlyForYou = (rememberedDeleteMessageOnlyForYou == 1); + _translateChatEnabled = (translateChatEnabled == 1); + _translateToRaw = int(QLocale::Language(translateToRaw)); } QString Settings::getSoundPath(const QString &key) const { @@ -1080,14 +1103,76 @@ float64 Settings::DefaultDialogsWidthRatio() { void Settings::setTranslateButtonEnabled(bool value) { _translateButtonEnabled = value; } + bool Settings::translateButtonEnabled() const { return _translateButtonEnabled; } -void Settings::setSkipTranslationForLanguages(std::vector languages) { - _skipTranslationForLanguages = std::move(languages); + +void Settings::setTranslateChatEnabled(bool value) { + _translateChatEnabled = value; } -std::vector Settings::skipTranslationForLanguages() const { - return _skipTranslationForLanguages; + +bool Settings::translateChatEnabled() const { + return _translateChatEnabled.current(); +} + +rpl::producer Settings::translateChatEnabledValue() const { + return _translateChatEnabled.value(); +} + +[[nodiscard]] const std::vector &DefaultSkipLanguages() { + using namespace Platform; + + static auto Result = [&] { + auto list = std::vector(); + list.push_back({ LanguageId::FromName(Lang::Id()) }); + const auto systemId = LanguageId::FromName(SystemLanguage()); + if (list.back() != systemId) { + list.push_back(systemId); + } + + Ensures(!list.empty()); + return list; + }(); + return Result; +} + +[[nodiscard]] std::vector NonEmptySkipList( + std::vector list) { + return list.empty() ? DefaultSkipLanguages() : list; +} + +void Settings::setTranslateTo(LanguageId id) { + _translateToRaw = int(id.value); +} + +LanguageId Settings::translateTo() const { + if (const auto raw = _translateToRaw.current()) { + return { QLocale::Language(raw) }; + } + return DefaultSkipLanguages().front(); +} + +rpl::producer Settings::translateToValue() const { + return _translateToRaw.value() | rpl::map([=](int raw) { + return raw + ? LanguageId{ QLocale::Language(raw) } + : DefaultSkipLanguages().front(); + }) | rpl::distinct_until_changed(); +} + +void Settings::setSkipTranslationLanguages( + std::vector languages) { + _skipTranslationLanguages = std::move(languages); +} + +auto Settings::skipTranslationLanguages() const -> std::vector { + return NonEmptySkipList(_skipTranslationLanguages.current()); +} + +auto Settings::skipTranslationLanguagesValue() const +-> rpl::producer> { + return _skipTranslationLanguages.value() | rpl::map(NonEmptySkipList); } void Settings::setRememberedDeleteMessageOnlyForYou(bool value) { diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 652269af1..d653f2f37 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "emoji.h" enum class RectPart; +struct LanguageId; namespace Ui { enum class InputSubmitSettings; @@ -99,6 +100,7 @@ public: static constexpr auto kDefaultVolume = 0.9; Settings(); + ~Settings(); [[nodiscard]] rpl::producer<> saveDelayedRequests() const { return _saveDelayed.events(); @@ -724,8 +726,16 @@ public: void setTranslateButtonEnabled(bool value); [[nodiscard]] bool translateButtonEnabled() const; - void setSkipTranslationForLanguages(std::vector languages); - [[nodiscard]] std::vector skipTranslationForLanguages() const; + void setTranslateChatEnabled(bool value); + [[nodiscard]] bool translateChatEnabled() const; + [[nodiscard]] rpl::producer translateChatEnabledValue() const; + void setTranslateTo(LanguageId id); + [[nodiscard]] LanguageId translateTo() const; + [[nodiscard]] rpl::producer translateToValue() const; + void setSkipTranslationLanguages(std::vector languages); + [[nodiscard]] std::vector skipTranslationLanguages() const; + [[nodiscard]] auto skipTranslationLanguagesValue() const + -> rpl::producer>; void setRememberedDeleteMessageOnlyForYou(bool value); [[nodiscard]] bool rememberedDeleteMessageOnlyForYou() const; @@ -845,7 +855,10 @@ private: HistoryView::DoubleClickQuickAction _chatQuickAction = HistoryView::DoubleClickQuickAction(); bool _translateButtonEnabled = false; - std::vector _skipTranslationForLanguages; + rpl::variable _translateChatEnabled = true; + rpl::variable _translateToRaw = 0; + rpl::variable> _skipTranslationLanguages; + rpl::event_stream<> _skipTranslationLanguagesChanges; bool _rememberedDeleteMessageOnlyForYou = false; bool _tabbedReplacedWithInfo = false; // per-window diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 6539bff93..7b75478e9 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -56,52 +56,53 @@ struct PeerUpdate { None = 0, // Common flags - Name = (1ULL << 0), - Username = (1ULL << 1), - Photo = (1ULL << 2), - About = (1ULL << 3), - Notifications = (1ULL << 4), - Migration = (1ULL << 5), - UnavailableReason = (1ULL << 6), - ChatThemeEmoji = (1ULL << 7), - IsBlocked = (1ULL << 8), - MessagesTTL = (1ULL << 9), - FullInfo = (1ULL << 10), - Usernames = (1ULL << 11), + Name = (1ULL << 0), + Username = (1ULL << 1), + Photo = (1ULL << 2), + About = (1ULL << 3), + Notifications = (1ULL << 4), + Migration = (1ULL << 5), + UnavailableReason = (1ULL << 6), + ChatThemeEmoji = (1ULL << 7), + IsBlocked = (1ULL << 8), + MessagesTTL = (1ULL << 9), + FullInfo = (1ULL << 10), + Usernames = (1ULL << 11), + TranslationDisabled = (1ULL << 12), // For users - CanShareContact = (1ULL << 12), - IsContact = (1ULL << 13), - PhoneNumber = (1ULL << 14), - OnlineStatus = (1ULL << 15), - BotCommands = (1ULL << 16), - BotCanBeInvited = (1ULL << 17), - BotStartToken = (1ULL << 18), - CommonChats = (1ULL << 19), - HasCalls = (1ULL << 20), - SupportInfo = (1ULL << 21), - IsBot = (1ULL << 22), - EmojiStatus = (1ULL << 23), + CanShareContact = (1ULL << 13), + IsContact = (1ULL << 14), + PhoneNumber = (1ULL << 15), + OnlineStatus = (1ULL << 16), + BotCommands = (1ULL << 17), + BotCanBeInvited = (1ULL << 18), + BotStartToken = (1ULL << 19), + CommonChats = (1ULL << 20), + HasCalls = (1ULL << 21), + SupportInfo = (1ULL << 22), + IsBot = (1ULL << 23), + EmojiStatus = (1ULL << 24), // For chats and channels - InviteLinks = (1ULL << 24), - Members = (1ULL << 25), - Admins = (1ULL << 26), - BannedUsers = (1ULL << 27), - Rights = (1ULL << 28), - PendingRequests = (1ULL << 29), - Reactions = (1ULL << 30), + InviteLinks = (1ULL << 25), + Members = (1ULL << 26), + Admins = (1ULL << 27), + BannedUsers = (1ULL << 28), + Rights = (1ULL << 29), + PendingRequests = (1ULL << 30), + Reactions = (1ULL << 31), // For channels - ChannelAmIn = (1ULL << 31), - StickersSet = (1ULL << 32), - ChannelLinkedChat = (1ULL << 33), - ChannelLocation = (1ULL << 34), - Slowmode = (1ULL << 35), - GroupCall = (1ULL << 36), + ChannelAmIn = (1ULL << 32), + StickersSet = (1ULL << 33), + ChannelLinkedChat = (1ULL << 34), + ChannelLocation = (1ULL << 35), + Slowmode = (1ULL << 36), + GroupCall = (1ULL << 37), // For iteration - LastUsedBit = (1ULL << 36), + LastUsedBit = (1ULL << 37), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -128,8 +129,10 @@ struct HistoryUpdate { OutboxRead = (1U << 10), BotKeyboard = (1U << 11), CloudDraft = (1U << 12), + TranslateFrom = (1U << 13), + TranslatedTo = (1U << 14), - LastUsedBit = (1U << 12), + LastUsedBit = (1U << 14), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index d8ffae90e..dddc44116 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -1040,6 +1040,7 @@ void ApplyChannelUpdate( } } channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); + channel->setTranslationDisabled(update.is_translations_disabled()); if (const auto allowed = update.vavailable_reactions()) { channel->setAllowedReactions(Data::Parse(*allowed)); } else { diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index fa6fe3c7c..1e19a00d1 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -470,6 +470,7 @@ void ApplyChatUpdate(not_null chat, const MTPDchatFull &update) { } chat->checkFolder(update.vfolder_id().value_or_empty()); chat->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); + chat->setTranslationDisabled(update.is_translations_disabled()); if (const auto allowed = update.vavailable_reactions()) { chat->setAllowedReactions(Data::Parse(*allowed)); } else { diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 7bee83b80..831f7b06c 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -655,6 +655,8 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const { const auto type = tr::lng_in_dlg_photo(tr::now); const auto caption = options.hideCaption ? TextWithEntities() + : options.translated + ? parent()->translatedText() : parent()->originalText(); const auto hasMiniImages = !images.empty(); return { @@ -899,6 +901,8 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const { }(); const auto caption = options.hideCaption ? TextWithEntities() + : options.translated + ? parent()->translatedText() : parent()->originalText(); const auto hasMiniImages = !images.empty(); return { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 76c7c3a3f..e5c36129e 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -546,6 +546,32 @@ void PeerData::checkFolder(FolderId folderId) { } } +void PeerData::setTranslationDisabled(bool disabled) { + const auto flag = disabled + ? TranslationFlag::Disabled + : TranslationFlag::Enabled; + if (_translationFlag != flag) { + _translationFlag = flag; + session().changes().peerUpdated( + this, + UpdateFlag::TranslationDisabled); + } +} + +PeerData::TranslationFlag PeerData::translationFlag() const { + return _translationFlag; +} + +void PeerData::saveTranslationDisabled(bool disabled) { + setTranslationDisabled(disabled); + + using Flag = MTPmessages_TogglePeerTranslations::Flag; + session().api().request(MTPmessages_TogglePeerTranslations( + MTP_flags(disabled ? Flag::f_disabled : Flag()), + input + )).send(); +} + void PeerData::setSettings(const MTPPeerSettings &data) { data.match([&](const MTPDpeerSettings &data) { _requestChatTitle = data.vrequest_chat_title().value_or_empty(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 5ee162bbf..3d966d1a3 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -349,6 +349,15 @@ public: return _requestChatDate; } + enum class TranslationFlag : uchar { + Unknown, + Disabled, + Enabled, + }; + void setTranslationDisabled(bool disabled); + [[nodiscard]] TranslationFlag translationFlag() const; + void saveTranslationDisabled(bool disabled); + void setSettings(const MTPPeerSettings &data); enum class BlockStatus : char { @@ -439,6 +448,7 @@ private: Settings _settings = PeerSettings(PeerSetting::Unknown); BlockStatus _blockStatus = BlockStatus::Unknown; LoadedStatus _loadedStatus = LoadedStatus::Not; + TranslationFlag _translationFlag = TranslationFlag::Unknown; bool _userpicHasVideo = false; QString _requestChatTitle; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 7af404965..faa1fd27d 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -288,6 +288,9 @@ enum class MessageFlag : uint64 { // Profile photo suggestion, views have special media type. IsUserpicSuggestion = (1ULL << 33), + + OnlyEmojiAndSpaces = (1ULL << 34), + OnlyEmojiAndSpacesSet = (1ULL << 35), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 8de9bd35b..ae0fd3c61 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -423,6 +423,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setCommonChatsCount(update.vcommon_chats_count().v); user->checkFolder(update.vfolder_id().value_or_empty()); user->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); + user->setTranslationDisabled(update.is_translations_disabled()); if (const auto info = user->botInfo.get()) { const auto group = update.vbot_group_admin_rights() diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 339b42ada..e4aaa7a61 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -9,12 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "history/view/history_view_item_preview.h" +#include "dialogs/dialogs_indexed_list.h" +#include "history/history_inner_widget.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" -#include "history/history_inner_widget.h" +#include "history/history_translation.h" #include "history/history_unread_things.h" -#include "dialogs/dialogs_indexed_list.h" #include "dialogs/ui/dialogs_layout.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" @@ -44,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "window/notifications_manager.h" #include "calls/calls_instance.h" +#include "spellcheck/spellcheck_types.h" #include "storage/localstorage.h" #include "storage/storage_facade.h" #include "storage/storage_shared_media.h" @@ -3456,6 +3458,46 @@ bool History::isTopPromoted() const { return (_flags & Flag::IsTopPromoted); } +void History::translateOfferFrom(LanguageId id) { + if (!id) { + if (translatedTo()) { + _translation->offerFrom(id); + } else if (_translation) { + _translation = nullptr; + session().changes().historyUpdated( + this, + UpdateFlag::TranslateFrom); + } + } else if (!_translation) { + _translation = std::make_unique(this, id); + } else { + _translation->offerFrom(id); + } +} + +LanguageId History::translateOfferedFrom() const { + return _translation ? _translation->offeredFrom() : LanguageId(); +} + +void History::translateTo(LanguageId id) { + if (!_translation) { + return; + } else if (!id && !translateOfferedFrom()) { + _translation = nullptr; + session().changes().historyUpdated(this, UpdateFlag::TranslatedTo); + } else { + _translation->translateTo(id); + } +} + +LanguageId History::translatedTo() const { + return _translation ? _translation->translatedTo() : LanguageId(); +} + +HistoryTranslation *History::translation() const { + return _translation.get(); +} + HistoryBlock::HistoryBlock(not_null history) : _history(history) { } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 3d36ad806..1d91860d6 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -18,9 +18,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; class HistoryBlock; +class HistoryTranslation; class HistoryItem; struct HistoryMessageMarkupData; class HistoryMainElementDelegateMixin; +struct LanguageId; namespace Main { class Session; @@ -425,6 +427,13 @@ public: [[nodiscard]] bool isTopPromoted() const; + void translateOfferFrom(LanguageId id); + [[nodiscard]] LanguageId translateOfferedFrom() const; + void translateTo(LanguageId id); + [[nodiscard]] LanguageId translatedTo() const; + + [[nodiscard]] HistoryTranslation *translation() const; + const not_null peer; // Still public data. @@ -617,6 +626,7 @@ private: HistoryBlock *block = nullptr; }; std::unique_ptr _buildingFrontBlock; + std::unique_ptr _translation; Data::HistoryDrafts _drafts; base::flat_map _acceptCloudDraftsAfter; @@ -628,6 +638,7 @@ private: HistoryView::SendActionPainter _sendActionPainter; + }; class HistoryBlock { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 069f785d0..c212cc7b2 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -57,6 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/message_field.h" #include "chat_helpers/emoji_interactions.h" #include "history/history_widget.h" +#include "history/view/history_view_translate_tracker.h" #include "base/platform/base_platform_info.h" #include "base/qt/qt_common_adapters.h" #include "base/qt/qt_key_modifiers.h" @@ -324,6 +325,7 @@ HistoryInner::HistoryInner( &controller->session(), [=](not_null view) { return itemTop(view); })) , _migrated(history->migrateFrom()) +, _translateTracker(std::make_unique(history)) , _pathGradient( HistoryView::MakePathShiftGradient( controller->chatStyle(), @@ -340,6 +342,7 @@ HistoryInner::HistoryInner( _history->delegateMixin()->setCurrent(this); if (_migrated) { _migrated->delegateMixin()->setCurrent(this); + _migrated->translateTo(_history->translatedTo()); } Window::ChatThemeValueFromPeer( @@ -431,7 +434,8 @@ HistoryInner::HistoryInner( session().changes().historyUpdates( _history, - Data::HistoryUpdate::Flag::OutboxRead + (Data::HistoryUpdate::Flag::OutboxRead + | Data::HistoryUpdate::Flag::TranslatedTo) ) | rpl::start_with_next([=] { update(); }, lifetime()); @@ -910,6 +914,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { const auto historyDisplayedEmpty = _history->isDisplayedEmpty() && (!_migrated || _migrated->isDisplayedEmpty()); + const auto translatedTo = _history->translatedTo(); if (_botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) { const auto st = context.st; const auto stm = &st->messageStyle(false, false); @@ -958,9 +963,11 @@ void HistoryInner::paintEvent(QPaintEvent *e) { return; } + _translateTracker->startBunch(); auto readTill = (HistoryItem*)nullptr; auto readContents = base::flat_set>(); const auto guard = gsl::finally([&] { + _translateTracker->finishBunch(); if (readTill && _widget->markingMessagesRead()) { session().data().histories().readInboxTill(readTill); } @@ -974,6 +981,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { not_null view, int top, int height) { + _translateTracker->add(view, translatedTo); const auto item = view->data(); const auto isSponsored = item->isSponsored(); const auto isUnread = !item->out() @@ -2414,7 +2422,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { [=] { copyContextText(itemId); }, &st::menuIconCopy); } - if (view->hasVisibleText() || mediaHasTextForCopy) { + if ((!item->translation() || !_history->translatedTo()) + && (view->hasVisibleText() || mediaHasTextForCopy)) { const auto translate = mediaHasTextForCopy ? (HistoryView::TransribedText(item) .append('\n') @@ -3925,6 +3934,7 @@ void HistoryInner::notifyIsBotChanged() { void HistoryInner::notifyMigrateUpdated() { _migrated = _history->migrateFrom(); + _migrated->translateTo(_history->translatedTo()); } void HistoryInner::applyDragSelection() { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 7a4430524..b81cc20bf 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -31,6 +31,7 @@ enum class CursorState : char; enum class PointState : char; class EmptyPainter; class Element; +class TranslateTracker; } // namespace HistoryView namespace HistoryView::Reactions { @@ -445,6 +446,7 @@ private: std::unique_ptr _botAbout; std::unique_ptr _emptyPainter; + std::unique_ptr _translateTracker; mutable History *_curHistory = nullptr; mutable int _curBlock = 0; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 9c5d78036..f44aba75f 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -78,6 +78,25 @@ constexpr auto kPinnedMessageTextLimit = 16; using ItemPreview = HistoryView::ItemPreview; +[[nodiscard]] bool IsOnlyEmojiAndSpaces(const QString &text) { + if (text.isEmpty()) { + return true; + } + auto emoji = 0; + auto start = text.data(); + const auto end = start + text.size(); + while (start < end) { + if (start->isSpace()) { + ++start; + } else if (Ui::Emoji::Find(start, end, &emoji)) { + start += emoji; + } else { + return false; + } + } + return true; +} + } // namespace void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) { @@ -2006,6 +2025,94 @@ std::optional HistoryItem::errorTextForForward( return {}; } +const HistoryMessageTranslation *HistoryItem::translation() const { + return Get(); +} + +bool HistoryItem::translationShowRequiresCheck(LanguageId to) const { + // Check if a call to translationShowRequiresRequest(to) is not a no-op. + if (!to) { + if (const auto translation = Get()) { + return (!translation->failed && translation->text.empty()) + || translation->used; + } + return false; + } else if (const auto translation = Get()) { + if (translation->to == to) { + return !translation->used && !translation->text.empty(); + } + return true; + } else { + return true; + } +} + +bool HistoryItem::translationShowRequiresRequest(LanguageId to) { + // When changing be sure to reflect in translationShowRequiresCheck(to). + if (!to) { + if (const auto translation = Get()) { + if (!translation->failed && translation->text.empty()) { + Assert(!translation->used); + RemoveComponents(HistoryMessageTranslation::Bit()); + } else { + translationToggle(translation, false); + } + } + return false; + } else if (const auto translation = Get()) { + if (translation->to == to) { + translationToggle(translation, true); + return false; + } + translationToggle(translation, false); + translation->to = to; + translation->requested = true; + translation->failed = false; + translation->text = {}; + return true; + } else { + AddComponents(HistoryMessageTranslation::Bit()); + const auto added = Get(); + added->to = to; + added->requested = true; + return true; + } +} + +void HistoryItem::translationToggle( + not_null translation, + bool used) { + if (translation->used != used && !translation->text.empty()) { + translation->used = used; + _history->owner().requestItemTextRefresh(this); + _history->owner().updateDependentMessages(this); + } +} + +void HistoryItem::translationDone(LanguageId to, TextWithEntities result) { + const auto set = [&](not_null translation) { + if (result.empty()) { + translation->failed = true; + } else { + translation->text = std::move(result); + if (_history->translatedTo() == to) { + translationToggle(translation, true); + } + } + }; + if (const auto translation = Get()) { + if (translation->to == to && translation->text.empty()) { + translation->requested = false; + set(translation); + } + } else { + AddComponents(HistoryMessageTranslation::Bit()); + const auto added = Get(); + added->to = to; + set(added); + } +} + bool HistoryItem::canReact() const { if (!isRegular() || isService()) { return false; @@ -2180,14 +2287,31 @@ MsgId HistoryItem::idOriginal() const { return id; } -TextWithEntities HistoryItem::originalText() const { - return isService() ? TextWithEntities() : _text; +const TextWithEntities &HistoryItem::originalText() const { + static const auto kEmpty = TextWithEntities(); + return isService() ? kEmpty : _text; } -TextWithEntities HistoryItem::originalTextWithLocalEntities() const { - return isService() - ? TextWithEntities() - : withLocalEntities(originalText()); +const TextWithEntities &HistoryItem::translatedText() const { + if (isService()) { + static const auto kEmpty = TextWithEntities(); + return kEmpty; + } else if (const auto translation = this->translation() + ; translation + && translation->used + && (translation->to == history()->translatedTo())) { + return translation->text; + } else { + return originalText(); + } +} + +TextWithEntities HistoryItem::translatedTextWithLocalEntities() const { + if (isService()) { + return {}; + } else { + return withLocalEntities(translatedText()); + } } TextForMimeData HistoryItem::clipboardText() const { @@ -2595,6 +2719,7 @@ void HistoryItem::setText(const TextWithEntities &textWithEntities) { void HistoryItem::setTextValue(TextWithEntities text) { const auto had = !_text.empty(); _text = std::move(text); + RemoveComponents(HistoryMessageTranslation::Bit()); if (had) { history()->owner().requestItemTextRefresh(this); } @@ -2658,7 +2783,7 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const { if (_media) { return _media->toPreview(options); } else if (!emptyText()) { - return { .text = _text }; + return { .text = options.translated ? translatedText() : _text }; } return {}; }(); @@ -2705,6 +2830,7 @@ TextWithEntities HistoryItem::inReplyText() const { return toPreview({ .hideSender = true, .generateImages = false, + .translated = true, }).text; } auto result = notificationText(); @@ -4256,7 +4382,7 @@ PreparedServiceText HistoryItem::preparePinnedText() { result.links.push_back(fromLink()); result.links.push_back(pinned->lnk); if (mediaText.isEmpty()) { - auto original = pinned->msg->originalText(); + auto original = pinned->msg->translatedText(); auto cutAt = 0; auto limit = kPinnedMessageTextLimit; auto size = original.text.size(); @@ -4544,6 +4670,23 @@ crl::time HistoryItem::getSelfDestructIn(crl::time now) { return 0; } +void HistoryItem::cacheOnlyEmojiAndSpaces(bool only) { + _flags |= MessageFlag::OnlyEmojiAndSpacesSet; + if (only) { + _flags |= MessageFlag::OnlyEmojiAndSpaces; + } else { + _flags &= ~MessageFlag::OnlyEmojiAndSpaces; + } +} + +bool HistoryItem::isOnlyEmojiAndSpaces() const { + if (!(_flags & MessageFlag::OnlyEmojiAndSpacesSet)) { + const_cast(this)->cacheOnlyEmojiAndSpaces( + IsOnlyEmojiAndSpaces(_text.text)); + } + return (_flags & MessageFlag::OnlyEmojiAndSpaces); +} + void HistoryItem::setupChatThemeChange() { if (const auto user = history()->peer->asUser()) { auto link = std::make_shared([=]( diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index bace65d08..47ab8f01a 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -21,10 +21,12 @@ struct HistoryMessageReply; struct HistoryMessageViews; struct HistoryMessageMarkupData; struct HistoryMessageReplyMarkup; +struct HistoryMessageTranslation; struct HistoryServiceDependentData; enum class HistorySelfDestructType; struct PreparedServiceText; class ReplyKeyboard; +struct LanguageId; namespace base { template @@ -259,6 +261,8 @@ public: [[nodiscard]] bool definesReplyKeyboard() const; [[nodiscard]] ReplyMarkupFlags replyKeyboardFlags() const; + void cacheOnlyEmojiAndSpaces(bool only); + [[nodiscard]] bool isOnlyEmojiAndSpaces() const; [[nodiscard]] bool hasSwitchInlineButton() const { return _flags & MessageFlag::HasSwitchInlineButton; } @@ -358,8 +362,9 @@ public: // Example: "[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text" [[nodiscard]] ItemPreview toPreview(ToPreviewOptions options) const; [[nodiscard]] TextWithEntities inReplyText() const; - [[nodiscard]] TextWithEntities originalText() const; - [[nodiscard]] TextWithEntities originalTextWithLocalEntities() const; + [[nodiscard]] const TextWithEntities &originalText() const; + [[nodiscard]] const TextWithEntities &translatedText() const; + [[nodiscard]] TextWithEntities translatedTextWithLocalEntities() const; [[nodiscard]] const std::vector &customTextLinks() const; [[nodiscard]] TextForMimeData clipboardText() const; @@ -397,6 +402,10 @@ public: [[nodiscard]] bool requiresSendInlineRight() const; [[nodiscard]] std::optional errorTextForForward( not_null to) const; + [[nodiscard]] const HistoryMessageTranslation *translation() const; + [[nodiscard]] bool translationShowRequiresCheck(LanguageId to) const; + bool translationShowRequiresRequest(LanguageId to); + void translationDone(LanguageId to, TextWithEntities result); [[nodiscard]] bool canReact() const; enum class ReactionSource { @@ -542,6 +551,9 @@ private: void setupChatThemeChange(); void setupTTLChange(); + void translationToggle( + not_null translation, + bool used); void setSelfDestruct(HistorySelfDestructType type, int ttlSeconds); TextWithEntities fromLinkText() const; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index d3b1f28d8..5fc0e6e1c 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_cloud_file.h" #include "history/history_item.h" +#include "spellcheck/spellcheck_types.h" // LanguageId. #include "ui/empty_userpic.h" #include "ui/effects/animations.h" #include "ui/chat/message_bubble.h" @@ -258,6 +259,15 @@ struct HistoryMessageReply }; +struct HistoryMessageTranslation + : public RuntimeComponent { + TextWithEntities text; + LanguageId to; + bool requested = false; + bool failed = false; + bool used = false; +}; + struct HistoryMessageReplyMarkup : public RuntimeComponent { using Button = HistoryMessageMarkupButton; diff --git a/Telegram/SourceFiles/history/history_translation.cpp b/Telegram/SourceFiles/history/history_translation.cpp new file mode 100644 index 000000000..be4febed4 --- /dev/null +++ b/Telegram/SourceFiles/history/history_translation.cpp @@ -0,0 +1,51 @@ +/* +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 "history/history_translation.h" + +#include "data/data_changes.h" +#include "history/history.h" +#include "main/main_session.h" + +namespace { + +using UpdateFlag = Data::HistoryUpdate::Flag; + +} // namespace + +HistoryTranslation::HistoryTranslation( + not_null history, + const LanguageId &offerFrom) +: _history(history) { + this->offerFrom(offerFrom); +} + +void HistoryTranslation::offerFrom(LanguageId id) { + if (_offerFrom == id) { + return; + } + _offerFrom = id; + auto &changes = _history->session().changes(); + changes.historyUpdated(_history, UpdateFlag::TranslateFrom); +} + +LanguageId HistoryTranslation::offeredFrom() const { + return _offerFrom; +} + +void HistoryTranslation::translateTo(LanguageId id) { + if (_translatedTo == id) { + return; + } + _translatedTo = id; + auto &changes = _history->session().changes(); + changes.historyUpdated(_history, UpdateFlag::TranslatedTo); +} + +LanguageId HistoryTranslation::translatedTo() const { + return _translatedTo; +} diff --git a/Telegram/SourceFiles/history/history_translation.h b/Telegram/SourceFiles/history/history_translation.h new file mode 100644 index 000000000..8a8f5f360 --- /dev/null +++ b/Telegram/SourceFiles/history/history_translation.h @@ -0,0 +1,32 @@ +/* +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 "spellcheck/spellcheck_types.h" + +class History; + +class HistoryTranslation final { +public: + HistoryTranslation( + not_null history, + const LanguageId &offerFrom); + + void offerFrom(LanguageId id); + [[nodiscard]] LanguageId offeredFrom() const; + + void translateTo(LanguageId id); + [[nodiscard]] LanguageId translatedTo() const; + +private: + const not_null _history; + + LanguageId _offerFrom; + LanguageId _translatedTo; + +}; \ No newline at end of file diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4d72a591b..c32370de8 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -102,6 +102,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_item_preview.h" #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_sticker_toast.h" +#include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" #include "info/info_memento.h" @@ -1488,6 +1489,9 @@ void HistoryWidget::orderWidgets() { if (_pinnedBar) { _pinnedBar->raise(); } + if (_translateBar) { + _translateBar->raise(); + } if (_requestsBar) { _requestsBar->raise(); } @@ -2093,6 +2097,7 @@ void HistoryWidget::showHistory( _history->showAtMsgId = _showAtMsgId; destroyUnreadBarOnClose(); + _translateBar = nullptr; _pinnedBar = nullptr; _pinnedTracker = nullptr; _groupCallBar = nullptr; @@ -2238,6 +2243,7 @@ void HistoryWidget::showHistory( _updateHistoryItems.cancel(); + setupTranslateBar(); setupPinnedTracker(); setupGroupCallBar(); setupRequestsBar(); @@ -3966,6 +3972,9 @@ void HistoryWidget::showAnimated( show(); _topBar->finishAnimating(); _cornerButtons.finishAnimations(); + if (_translateBar) { + _translateBar->finishAnimating(); + } if (_pinnedBar) { _pinnedBar->finishAnimating(); } @@ -4029,6 +4038,9 @@ void HistoryWidget::doneShow() { _preserveScrollTop = true; preloadHistoryIfNeeded(); updatePinnedViewer(); + if (_translateBar) { + _translateBar->finishAnimating(); + } if (_pinnedBar) { _pinnedBar->finishAnimating(); } @@ -5320,7 +5332,12 @@ void HistoryWidget::updateControlsGeometry() { _requestsBar->move(0, requestsTop); _requestsBar->resizeToWidth(width()); } - const auto pinnedBarTop = requestsTop + (_requestsBar ? _requestsBar->height() : 0); + const auto translateTop = requestsTop + (_requestsBar ? _requestsBar->height() : 0); + if (_translateBar) { + _translateBar->move(0, translateTop); + _translateBar->resizeToWidth(width()); + } + const auto pinnedBarTop = translateTop + (_translateBar ? _translateBar->height() : 0); if (_pinnedBar) { _pinnedBar->move(0, pinnedBarTop); _pinnedBar->resizeToWidth(width()); @@ -5499,6 +5516,9 @@ void HistoryWidget::updateHistoryGeometry( } auto newScrollHeight = height() - _topBar->height(); + if (_translateBar) { + newScrollHeight -= _translateBar->height(); + } if (_pinnedBar) { newScrollHeight -= _pinnedBar->height(); } @@ -6229,6 +6249,40 @@ void HistoryWidget::checkLastPinnedClickedIdReset( } } +void HistoryWidget::setupTranslateBar() { + Expects(_history != nullptr); + + _translateBar = std::make_unique( + this, + _history); + + controller()->adaptive().oneColumnValue( + ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { + raw->setShadowGeometryPostprocess([=](QRect geometry) { + if (!one) { + geometry.setLeft(geometry.left() + st::lineWidth); + } + return geometry; + }); + }, _translateBar->lifetime()); + + _translateBarHeight = 0; + _translateBar->heightValue( + ) | rpl::start_with_next([=](int height) { + _topDelta = _preserveScrollTop ? 0 : (height - _translateBarHeight); + _translateBarHeight = height; + updateHistoryGeometry(); + updateControlsGeometry(); + _topDelta = 0; + }, _translateBar->lifetime()); + + orderWidgets(); + + if (_showAnimation) { + _translateBar->hide(); + } +} + void HistoryWidget::setupPinnedTracker() { Expects(_history != nullptr); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 67c81174a..aa0c827aa 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -91,6 +91,7 @@ class TopBarWidget; class ContactStatus; class Element; class PinnedTracker; +class TranslateBar; class ComposeSearch; namespace Controls { class RecordLock; @@ -494,6 +495,7 @@ private: void updateReplyEditText(not_null item); void updatePinnedViewer(); + void setupTranslateBar(); void setupPinnedTracker(); void checkPinnedBarState(); void clearHidingPinnedBar(); @@ -638,6 +640,9 @@ private: object_ptr _fieldBarCancel; + std::unique_ptr _translateBar; + int _translateBarHeight = 0; + std::unique_ptr _pinnedTracker; std::unique_ptr _pinnedBar; std::unique_ptr _hidingPinnedBar; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 7ee998ee2..ecb612461 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "spellcheck/spellcheck_types.h" #include "apiwrap.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" @@ -1058,7 +1059,8 @@ base::unique_qptr FillContextMenu( .append('\n') .append(item->originalText())) : item->originalText(); - if (!translate.text.isEmpty() + if ((!item->translation() || !item->history()->translatedTo()) + && !translate.text.isEmpty() && !Ui::SkipTranslate(translate)) { result->addAction(tr::lng_context_translate(tr::now), [=] { if (const auto item = owner->message(itemId)) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index b7f60e954..8477dcfd5 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -652,6 +652,18 @@ const Ui::Text::String &Element::text() const { return _text; } +OnlyEmojiAndSpaces Element::isOnlyEmojiAndSpaces() const { + if (data()->Has()) { + return OnlyEmojiAndSpaces::No; + } else if (!_text.isEmpty() || data()->originalText().empty()) { + return _text.isOnlyEmojiAndSpaces() + ? OnlyEmojiAndSpaces::Yes + : OnlyEmojiAndSpaces::No; + } else { + return OnlyEmojiAndSpaces::Unknown; + } +} + int Element::textHeightFor(int textWidth) { validateText(); if (_textWidth != textWidth) { @@ -837,7 +849,7 @@ void Element::validateText() { }; _text.setMarkedText( st::messageTextStyle, - item->originalTextWithLocalEntities(), + item->translatedTextWithLocalEntities(), Ui::ItemTextOptions(item), context); if (!text.empty() && _text.isEmpty()) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index ec6807995..7c2ab77e7 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -58,6 +58,12 @@ enum class Context : char { ContactPreview }; +enum class OnlyEmojiAndSpaces : char { + Unknown, + Yes, + No, +}; + class Element; class ElementDelegate { public: @@ -309,6 +315,8 @@ public: [[nodiscard]] Ui::Text::IsolatedEmoji isolatedEmoji() const; [[nodiscard]] Ui::Text::OnlyCustomEmoji onlyCustomEmoji() const; + [[nodiscard]] OnlyEmojiAndSpaces isOnlyEmojiAndSpaces() const; + // For blocks context this should be called only from recountAttachToPreviousInBlocks(). void setAttachToPrevious(bool attachToNext, Element *previous = nullptr); diff --git a/Telegram/SourceFiles/history/view/history_view_item_preview.h b/Telegram/SourceFiles/history/view/history_view_item_preview.h index a2229cddf..4348743b3 100644 --- a/Telegram/SourceFiles/history/view/history_view_item_preview.h +++ b/Telegram/SourceFiles/history/view/history_view_item_preview.h @@ -37,6 +37,7 @@ struct ToPreviewOptions { bool generateImages = true; bool ignoreGroup = false; bool ignoreTopic = true; + bool translated = false; }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 8db15853c..d2dd7519a 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -295,17 +295,6 @@ RepliesWidget::RepliesWidget( searchInTopic(); }, _topBar->lifetime()); - if (_rootView) { - _rootView->raise(); - } - if (_pinnedBar) { - _pinnedBar->raise(); - } - if (_topicReopenBar) { - _topicReopenBar->bar().raise(); - } - _topBarShadow->raise(); - controller->adaptive().value( ) | rpl::start_with_next([=] { updateAdaptiveLayout(); @@ -426,6 +415,9 @@ void RepliesWidget::orderWidgets() { if (_pinnedBar) { _pinnedBar->raise(); } + if (_topicReopenBar) { + _topicReopenBar->bar().raise(); + } _topBarShadow->raise(); _composeControls->raisePanels(); } diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp new file mode 100644 index 000000000..cdf6e4de6 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -0,0 +1,197 @@ +/* +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 "history/view/history_view_translate_bar.h" + +#include "boxes/translate_box.h" // Ui::LanguageName. +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_changes.h" +#include "history/history.h" +#include "main/main_session.h" +#include "spellcheck/spellcheck_types.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "styles/style_chat.h" + +#include + +namespace HistoryView { + +TranslateBar::TranslateBar( + not_null parent, + not_null history) +: _wrap(parent, object_ptr( + parent, + QString(), + st::historyComposeButton)) +, _shadow(std::make_unique(_wrap.parentWidget())) { + _wrap.hide(anim::type::instant); + _shadow->hide(); + + setup(history); +} + +TranslateBar::~TranslateBar() = default; + +void TranslateBar::updateControlsGeometry(QRect wrapGeometry) { + const auto hidden = _wrap.isHidden() || !wrapGeometry.height(); + if (_shadow->isHidden() != hidden) { + _shadow->setVisible(!hidden); + } +} + +void TranslateBar::setShadowGeometryPostprocess( + Fn postprocess) { + _shadowGeometryPostprocess = std::move(postprocess); + updateShadowGeometry(_wrap.geometry()); +} + +void TranslateBar::updateShadowGeometry(QRect wrapGeometry) { + const auto regular = QRect( + wrapGeometry.x(), + wrapGeometry.y() + wrapGeometry.height(), + wrapGeometry.width(), + st::lineWidth); + _shadow->setGeometry(_shadowGeometryPostprocess + ? _shadowGeometryPostprocess(regular) + : regular); +} + +void TranslateBar::setup(not_null history) { + _wrap.geometryValue( + ) | rpl::start_with_next([=](QRect rect) { + updateShadowGeometry(rect); + updateControlsGeometry(rect); + }, _wrap.lifetime()); + + const auto button = static_cast(_wrap.entity()); + button->setClickedCallback([=] { + const auto to = history->translatedTo() + ? LanguageId() + : Core::App().settings().translateTo(); + history->translateTo(to); + if (const auto migrated = history->migrateFrom()) { + migrated->translateTo(to); + } + }); + + const auto label = Ui::CreateChild( + button, + st::historyTranslateLabel); + const auto icon = Ui::CreateChild(button); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + icon->resize(st::historyTranslateIcon.size()); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + st::historyTranslateIcon.paint(p, 0, 0, icon->width()); + }, icon->lifetime()); + const auto settings = Ui::CreateChild( + button, + st::historyTranslateSettings); + const auto updateLabelGeometry = [=] { + const auto full = _wrap.width() - icon->width(); + const auto skip = st::semiboldFont->spacew * 2; + const auto natural = label->naturalWidth(); + const auto top = [&] { + return (_wrap.height() - label->height()) / 2; + }; + if (natural <= full - 2 * (settings->width() + skip)) { + label->resizeToWidth(natural); + label->moveToRight((full - label->width()) / 2, top()); + } else { + const auto available = full - settings->width() - 2 * skip; + label->resizeToWidth(std::min(natural, available)); + label->moveToRight(settings->width() + skip, top()); + } + icon->move( + label->x() - icon->width(), + (_wrap.height() - icon->height()) / 2); + }; + + _wrap.sizeValue() | rpl::start_with_next([=](QSize size) { + settings->moveToRight(0, 0, size.width()); + updateLabelGeometry(); + }, lifetime()); + + rpl::combine( + Core::App().settings().translateToValue(), + history->session().changes().historyFlagsValue( + history, + (Data::HistoryUpdate::Flag::TranslatedTo + | Data::HistoryUpdate::Flag::TranslateFrom)) + ) | rpl::map([=](LanguageId to, const auto&) { + return history->translatedTo() + ? u"Show Original"_q + : history->translateOfferedFrom() + ? u"Translate to "_q + Ui::LanguageName(to.locale()) + : QString(); + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](QString phrase) { + _shouldBeShown = !phrase.isEmpty(); + if (_shouldBeShown) { + label->setText(phrase); + updateLabelGeometry(); + } + if (!_forceHidden) { + _wrap.toggle(_shouldBeShown, anim::type::normal); + } + }, lifetime()); +} + +void TranslateBar::show() { + if (!_forceHidden) { + return; + } + _forceHidden = false; + if (_shouldBeShown) { + _wrap.show(anim::type::instant); + _shadow->show(); + } +} + +void TranslateBar::hide() { + if (_forceHidden) { + return; + } + _forceHidden = true; + _wrap.hide(anim::type::instant); + _shadow->hide(); +} + +void TranslateBar::raise() { + _wrap.raise(); + _shadow->raise(); +} + +void TranslateBar::finishAnimating() { + _wrap.finishAnimating(); +} + +void TranslateBar::move(int x, int y) { + _wrap.move(x, y); +} + +void TranslateBar::resizeToWidth(int width) { + _wrap.entity()->resizeToWidth(width); +} + +int TranslateBar::height() const { + return !_forceHidden + ? _wrap.height() + : _shouldBeShown + ? st::historyReplyHeight + : 0; +} + +rpl::producer TranslateBar::heightValue() const { + return _wrap.heightValue(); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.h b/Telegram/SourceFiles/history/view/history_view_translate_bar.h new file mode 100644 index 000000000..4f580f463 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.h @@ -0,0 +1,55 @@ +/* +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 "ui/wrap/slide_wrap.h" + +class History; +struct LanguageId; + +namespace Ui { +class PlainShadow; +} // namespace Ui + +namespace HistoryView { + +class TranslateBar final { +public: + TranslateBar(not_null parent, not_null history); + ~TranslateBar(); + + void show(); + void hide(); + void raise(); + void finishAnimating(); + + void setShadowGeometryPostprocess(Fn postprocess); + + void move(int x, int y); + void resizeToWidth(int width); + [[nodiscard]] int height() const; + [[nodiscard]] rpl::producer heightValue() const; + + [[nodiscard]] rpl::lifetime &lifetime() { + return _wrap.lifetime(); + } + +private: + void setup(not_null history); + void updateShadowGeometry(QRect wrapGeometry); + void updateControlsGeometry(QRect wrapGeometry); + + Ui::SlideWrap<> _wrap; + std::unique_ptr _shadow; + Fn _shadowGeometryPostprocess; + bool _shouldBeShown = false; + bool _forceHidden = false; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp new file mode 100644 index 000000000..445388205 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp @@ -0,0 +1,351 @@ +/* +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 "history/view/history_view_translate_tracker.h" + +#include "apiwrap.h" +#include "api/api_text_entities.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_changes.h" +#include "data/data_peer_values.h" // Data::AmPremiumValue. +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "history/view/history_view_element.h" +#include "main/main_session.h" +#include "spellcheck/spellcheck_types.h" +#include "spellcheck/platform/platform_language.h" + +namespace HistoryView { +namespace { + +constexpr auto kEnoughForRecognition = 10; +constexpr auto kEnoughForTranslation = 6; +constexpr auto kRequestLengthLimit = 24 * 1024; +constexpr auto kRequestCountLimit = 20; + +} // namespace + +struct TranslateTracker::ItemForRecognize { + uint64 generation = 0; + MaybeLanguageId id; +}; + +TranslateTracker::TranslateTracker(not_null history) +: _history(history) +, _limit(kEnoughForRecognition) { + setup(); +} + +TranslateTracker::~TranslateTracker() { + cancelToRequest(); + cancelSentRequest(); +} + +rpl::producer TranslateTracker::trackingLanguage() const { + return _trackingLanguage.value(); +} + +void TranslateTracker::setup() { + const auto peer = _history->peer; + const auto session = &_history->session(); + peer->updateFull(); + + auto translationEnabled = session->changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::TranslationDisabled + ) | rpl::map([=] { + return peer->translationFlag() == PeerData::TranslationFlag::Enabled; + }) | rpl::distinct_until_changed(); + + _trackingLanguage = Data::AmPremiumValue(&_history->session()); + + _trackingLanguage.value( + ) | rpl::start_with_next([=](bool tracking) { + _trackingLifetime.destroy(); + if (tracking) { + recognizeCollected(); + trackSkipLanguages(); + } else { + checkRecognized({}); + _history->translateTo({}); + if (const auto migrated = _history->migrateFrom()) { + migrated->translateTo({}); + } + } + }, _lifetime); +} + +bool TranslateTracker::enoughForRecognition() const { + return _itemsForRecognize.size() >= kEnoughForRecognition; +} + +void TranslateTracker::startBunch() { + _addedInBunch = 0; + ++_generation; +} + +void TranslateTracker::add( + not_null view, + LanguageId translatedTo) { + const auto item = view->data(); + const auto only = view->isOnlyEmojiAndSpaces(); + if (only != OnlyEmojiAndSpaces::Unknown) { + item->cacheOnlyEmojiAndSpaces(only == OnlyEmojiAndSpaces::Yes); + } + add(item, translatedTo, false); +} + +void TranslateTracker::add( + not_null item, + LanguageId translatedTo) { + add(item, translatedTo, false); +} + +void TranslateTracker::add( + not_null item, + LanguageId translatedTo, + bool skipDependencies) { + Expects(_addedInBunch >= 0); + + if (item->out() + || item->isService() + || !item->isRegular() + || item->isOnlyEmojiAndSpaces()) { + return; + } + if (item->translationShowRequiresCheck(translatedTo)) { + _switchTranslations[item] = translatedTo; + } + if (!skipDependencies) { + if (const auto reply = item->Get()) { + if (const auto to = reply->replyToMsg.get()) { + add(to, translatedTo, true); + } + } + } + const auto id = item->fullId(); + const auto i = _itemsForRecognize.find(id); + if (i != end(_itemsForRecognize)) { + i->second.generation = _generation; + return; + } + const auto &text = item->originalText().text; + _itemsForRecognize.emplace(id, ItemForRecognize{ + .generation = _generation, + .id = (_trackingLanguage.current() + ? Platform::Language::Recognize(text) + : MaybeLanguageId{ text }), + }); + ++_addedInBunch; +} + +void TranslateTracker::switchTranslation( + not_null item, + LanguageId id) { + if (item->translationShowRequiresRequest(id)) { + _itemsToRequest.emplace( + item->fullId(), + ItemToRequest{ item->originalText().text.size() }); + } +} + +void TranslateTracker::finishBunch() { + if (_addedInBunch > 0) { + accumulate_max(_limit, _addedInBunch + kEnoughForRecognition); + _addedInBunch = -1; + applyLimit(); + if (_trackingLanguage.current()) { + checkRecognized(); + } + } + requestSome(); + if (!_switchTranslations.empty()) { + auto switching = base::take(_switchTranslations); + for (const auto &[item, id] : switching) { + switchTranslation(item, id); + } + _switchTranslations = std::move(switching); + _switchTranslations.clear(); + } +} + +void TranslateTracker::cancelToRequest() { + if (!_itemsToRequest.empty()) { + const auto owner = &_history->owner(); + for (const auto &[id, entry] : base::take(_itemsToRequest)) { + if (const auto item = owner->message(id)) { + item->translationShowRequiresRequest({}); + } + } + } +} + +void TranslateTracker::cancelSentRequest() { + if (_requestId) { + const auto owner = &_history->owner(); + for (const auto &id : base::take(_requested)) { + if (const auto item = owner->message(id)) { + item->translationShowRequiresRequest({}); + } + } + _history->session().api().request(base::take(_requestId)).cancel(); + } +} + +void TranslateTracker::requestSome() { + if (_requestId || _itemsToRequest.empty()) { + return; + } + const auto to = _history->translatedTo(); + if (!to) { + cancelToRequest(); + return; + } + _requested.clear(); + _requested.reserve(_itemsToRequest.size()); + const auto session = &_history->session(); + const auto peerId = _itemsToRequest.back().first.peer; + auto peer = (peerId == _history->peer->id) + ? _history->peer + : session->data().peer(peerId); + auto length = 0; + auto list = QVector(); + list.reserve(_itemsToRequest.size()); + for (auto i = _itemsToRequest.end(); i != _itemsToRequest.begin();) { + if ((--i)->first.peer != peerId) { + break; + } + length += i->second.length; + _requested.push_back(i->first); + list.push_back(MTP_int(i->first.msg)); + i = _itemsToRequest.erase(i); + if (list.size() >= kRequestCountLimit + || length >= kRequestLengthLimit) { + break; + } + } + using Flag = MTPmessages_TranslateText::Flag; + _requestId = session->api().request(MTPmessages_TranslateText( + MTP_flags(Flag::f_peer | Flag::f_id), + peer->input, + MTP_vector(list), + MTPVector(), + MTP_string(to.locale().name().mid(0, 2)) + )).done([=](const MTPmessages_TranslatedText &result) { + requestDone(to, result.data().vresult().v); + }).fail([=] { + requestDone(to, {}); + }).send(); +} + +void TranslateTracker::requestDone( + LanguageId to, + const QVector &list) { + auto index = 0; + const auto session = &_history->session(); + const auto owner = &session->data(); + for (const auto &id : base::take(_requested)) { + if (const auto item = owner->message(id)) { + const auto data = (index >= list.size()) + ? nullptr + : &list[index].data(); + auto text = data ? TextWithEntities{ + qs(data->vtext()), + Api::EntitiesFromMTP(session, data->ventities().v) + } : TextWithEntities(); + item->translationDone(to, std::move(text)); + } + ++index; + } + _requestId = 0; + requestSome(); +} + +void TranslateTracker::applyLimit() { + const auto generationProjection = [](const auto &pair) { + return pair.second.generation; + }; + const auto owner = &_history->owner(); + + // Erase starting with oldest generation till items count is not too big. + while (_itemsForRecognize.size() > _limit) { + const auto oldest = ranges::min_element( + _itemsForRecognize, + ranges::less(), + generationProjection + )->second.generation; + for (auto i = begin(_itemsForRecognize) + ; i != end(_itemsForRecognize);) { + if (i->second.generation == oldest) { + if (const auto j = _itemsToRequest.find(i->first) + ; j != end(_itemsToRequest)) { + if (const auto item = owner->message(i->first)) { + item->translationShowRequiresRequest({}); + } + _itemsToRequest.erase(j); + } + i = _itemsForRecognize.erase(i); + } else { + ++i; + } + } + } +} + +void TranslateTracker::recognizeCollected() { + const auto owner = &_history->owner(); + for (auto &[id, entry] : _itemsForRecognize) { + if (const auto text = std::get_if(&entry.id)) { + entry.id = Platform::Language::Recognize(*text); + } + } +} + +void TranslateTracker::trackSkipLanguages() { + Core::App().settings().skipTranslationLanguagesValue( + ) | rpl::start_with_next([=](const std::vector &skip) { + checkRecognized(skip); + }, _trackingLifetime); +} + +void TranslateTracker::checkRecognized() { + checkRecognized(Core::App().settings().skipTranslationLanguages()); +} + +void TranslateTracker::checkRecognized(const std::vector &skip) { + if (!_trackingLanguage.current()) { + _history->translateOfferFrom({}); + return; + } + auto languages = base::flat_map(); + for (const auto &[id, entry] : _itemsForRecognize) { + if (const auto id = std::get_if(&entry.id)) { + if (*id && !ranges::contains(skip, *id)) { + ++languages[*id]; + } + } + } + using namespace base; + const auto count = int(_itemsForRecognize.size()); + constexpr auto p = &flat_multi_map_pair_type::second; + const auto threshold = (count > kEnoughForRecognition) + ? (count * kEnoughForTranslation / kEnoughForRecognition) + : _allLoaded + ? std::min(count, kEnoughForTranslation) + : kEnoughForTranslation; + if (ranges::accumulate(languages, 0, ranges::plus(), p) >= threshold) { + _history->translateOfferFrom( + ranges::max_element(languages, ranges::less(), p)->first); + } else { + _history->translateOfferFrom({}); + } +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_translate_tracker.h b/Telegram/SourceFiles/history/view/history_view_translate_tracker.h new file mode 100644 index 000000000..8294bf5a8 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_translate_tracker.h @@ -0,0 +1,76 @@ +/* +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 + +class History; +class HistoryItem; +struct LanguageId; + +namespace HistoryView { + +class Element; + +class TranslateTracker final { +public: + explicit TranslateTracker(not_null history); + ~TranslateTracker(); + + [[nodiscard]] bool enoughForRecognition() const; + void startBunch(); + void add(not_null view, LanguageId translatedTo); + void add(not_null item, LanguageId translatedTo); + void finishBunch(); + + [[nodiscard]] rpl::producer trackingLanguage() const; + +private: + using MaybeLanguageId = std::variant; + struct ItemForRecognize; + struct ItemToRequest { + int length = 0; + }; + + void setup(); + void add( + not_null item, + LanguageId translatedTo, + bool skipDependencies); + void recognizeCollected(); + void trackSkipLanguages(); + void checkRecognized(); + void checkRecognized(const std::vector &skip); + void applyLimit(); + void requestSome(); + void cancelToRequest(); + void cancelSentRequest(); + void switchTranslation(not_null item, LanguageId id); + + void requestDone( + LanguageId to, + const QVector &list); + + const not_null _history; + rpl::variable _trackingLanguage = false; + base::flat_map _itemsForRecognize; + uint64 _generation = 0; + int _limit = 0; + int _addedInBunch = -1; + bool _allLoaded = false; + + base::flat_map, LanguageId> _switchTranslations; + base::flat_map _itemsToRequest; + std::vector _requested; + mtpRequestId _requestId = 0; + + rpl::lifetime _trackingLifetime; + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView + diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index e20b8e76e..353aa24fc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -305,7 +305,7 @@ Ui::Text::String Media::createCaption(not_null item) const { }; result.setMarkedText( st::messageTextStyle, - item->originalTextWithLocalEntities(), + item->translatedTextWithLocalEntities(), Ui::ItemTextOptions(item), context); FillTextWithAnimatedSpoilers(_parent, result); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 219c0d0b0..bf2e83d62 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -2300,7 +2300,7 @@ void OverlayWidget::refreshCaption() { return; } } - const auto caption = _message->originalText(); + const auto caption = _message->translatedText(); if (caption.text.isEmpty()) { return; } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 03c779c86..7ffd941c7 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1257,3 +1257,19 @@ historyHasCustomEmoji: FlatLabel(defaultFlatLabel) { minWidth: 80px; } historyHasCustomEmojiPosition: point(12px, 4px); + +historyTranslateLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: windowActiveTextFg; + minWidth: 80px; +} +historyTranslateIcon: icon{{ "menu/translate", windowActiveTextFg }}; +historyTranslateSettings: IconButton(defaultIconButton) { + width: 46px; + height: 46px; + icon: icon{{ "menu/customize", windowActiveTextFg }}; + iconOver: icon{{ "menu/customize", windowActiveTextFg }}; + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 38px; + ripple: defaultRippleAnimation; +} diff --git a/Telegram/lib_spellcheck b/Telegram/lib_spellcheck index cf59ca87b..e5ac664fe 160000 --- a/Telegram/lib_spellcheck +++ b/Telegram/lib_spellcheck @@ -1 +1 @@ -Subproject commit cf59ca87b761ab5bbd80be02a61cd38a70142898 +Subproject commit e5ac664fe397d5874a244bbbc8a7b266223cb88b diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 43e912801..f2e698f22 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 43e9128014c5239a6732ae34bdfe007efb9692c8 +Subproject commit f2e698f2209a86c133261196275ca98273c7a4dc