Initial chat-translation feature implementation.

This commit is contained in:
John Preston 2023-01-26 19:36:43 +04:00
parent 02a0ca7112
commit 719466fcac
40 changed files with 1356 additions and 141 deletions

View File

@ -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

View File

@ -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 <QtGui/QClipboard>
namespace {
namespace {
[[nodiscard]] std::vector<QLocale> 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<QLocale> 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);

View File

@ -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<Qt::MouseButton> ShowButton::clicks() const {
} // namespace
namespace Translate {
std::vector<QLocale> 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<MTP::Sender>(
&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<MTPTextWithEntities>(1, MTP_textWithEntities(
MTP_string(text.text),
MTP_vector<MTPMessageEntity>()))),
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<QLocale> 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<QLocale>()));
});
}
@ -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

View File

@ -10,9 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class PeerData;
namespace Ui {
namespace Translate {
[[nodiscard]] std::vector<QLocale> LocalesFromSettings();
} // namespace Translate
class GenericBox;

View File

@ -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

View File

@ -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<RecentEmojiPreload>();
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<int> skipTranslationForLanguages;
qint32 skipTranslationLanguagesCount = 0;
std::vector<LanguageId> 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<int> languages) {
_skipTranslationForLanguages = std::move(languages);
void Settings::setTranslateChatEnabled(bool value) {
_translateChatEnabled = value;
}
std::vector<int> Settings::skipTranslationForLanguages() const {
return _skipTranslationForLanguages;
bool Settings::translateChatEnabled() const {
return _translateChatEnabled.current();
}
rpl::producer<bool> Settings::translateChatEnabledValue() const {
return _translateChatEnabled.value();
}
[[nodiscard]] const std::vector<LanguageId> &DefaultSkipLanguages() {
using namespace Platform;
static auto Result = [&] {
auto list = std::vector<LanguageId>();
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<LanguageId> NonEmptySkipList(
std::vector<LanguageId> 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<LanguageId> 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<LanguageId> languages) {
_skipTranslationLanguages = std::move(languages);
}
auto Settings::skipTranslationLanguages() const -> std::vector<LanguageId> {
return NonEmptySkipList(_skipTranslationLanguages.current());
}
auto Settings::skipTranslationLanguagesValue() const
-> rpl::producer<std::vector<LanguageId>> {
return _skipTranslationLanguages.value() | rpl::map(NonEmptySkipList);
}
void Settings::setRememberedDeleteMessageOnlyForYou(bool value) {

View File

@ -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<int> languages);
[[nodiscard]] std::vector<int> skipTranslationForLanguages() const;
void setTranslateChatEnabled(bool value);
[[nodiscard]] bool translateChatEnabled() const;
[[nodiscard]] rpl::producer<bool> translateChatEnabledValue() const;
void setTranslateTo(LanguageId id);
[[nodiscard]] LanguageId translateTo() const;
[[nodiscard]] rpl::producer<LanguageId> translateToValue() const;
void setSkipTranslationLanguages(std::vector<LanguageId> languages);
[[nodiscard]] std::vector<LanguageId> skipTranslationLanguages() const;
[[nodiscard]] auto skipTranslationLanguagesValue() const
-> rpl::producer<std::vector<LanguageId>>;
void setRememberedDeleteMessageOnlyForYou(bool value);
[[nodiscard]] bool rememberedDeleteMessageOnlyForYou() const;
@ -845,7 +855,10 @@ private:
HistoryView::DoubleClickQuickAction _chatQuickAction =
HistoryView::DoubleClickQuickAction();
bool _translateButtonEnabled = false;
std::vector<int> _skipTranslationForLanguages;
rpl::variable<bool> _translateChatEnabled = true;
rpl::variable<int> _translateToRaw = 0;
rpl::variable<std::vector<LanguageId>> _skipTranslationLanguages;
rpl::event_stream<> _skipTranslationLanguagesChanges;
bool _rememberedDeleteMessageOnlyForYou = false;
bool _tabbedReplacedWithInfo = false; // per-window

View File

@ -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<Flag>;
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<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View File

@ -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 {

View File

@ -470,6 +470,7 @@ void ApplyChatUpdate(not_null<ChatData*> 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 {

View File

@ -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 {

View File

@ -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();

View File

@ -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;

View File

@ -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<MessageFlag>;

View File

@ -423,6 +423,7 @@ void ApplyUserUpdate(not_null<UserData*> 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()

View File

@ -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<HistoryTranslation>(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(history) {
}

View File

@ -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<PeerData*> peer;
// Still public data.
@ -617,6 +626,7 @@ private:
HistoryBlock *block = nullptr;
};
std::unique_ptr<BuildingBlock> _buildingFrontBlock;
std::unique_ptr<HistoryTranslation> _translation;
Data::HistoryDrafts _drafts;
base::flat_map<MsgId, TimeId> _acceptCloudDraftsAfter;
@ -628,6 +638,7 @@ private:
HistoryView::SendActionPainter _sendActionPainter;
};
class HistoryBlock {

View File

@ -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<const Element*> view) { return itemTop(view); }))
, _migrated(history->migrateFrom())
, _translateTracker(std::make_unique<HistoryView::TranslateTracker>(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<not_null<HistoryItem*>>();
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<Element*> 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() {

View File

@ -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> _botAbout;
std::unique_ptr<HistoryView::EmptyPainter> _emptyPainter;
std::unique_ptr<HistoryView::TranslateTracker> _translateTracker;
mutable History *_curHistory = nullptr;
mutable int _curBlock = 0;

View File

@ -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<QString> HistoryItem::errorTextForForward(
return {};
}
const HistoryMessageTranslation *HistoryItem::translation() const {
return Get<HistoryMessageTranslation>();
}
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<HistoryMessageTranslation>()) {
return (!translation->failed && translation->text.empty())
|| translation->used;
}
return false;
} else if (const auto translation = Get<HistoryMessageTranslation>()) {
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<HistoryMessageTranslation>()) {
if (!translation->failed && translation->text.empty()) {
Assert(!translation->used);
RemoveComponents(HistoryMessageTranslation::Bit());
} else {
translationToggle(translation, false);
}
}
return false;
} else if (const auto translation = Get<HistoryMessageTranslation>()) {
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<HistoryMessageTranslation>();
added->to = to;
added->requested = true;
return true;
}
}
void HistoryItem::translationToggle(
not_null<HistoryMessageTranslation*> 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<HistoryMessageTranslation*> 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<HistoryMessageTranslation>()) {
if (translation->to == to && translation->text.empty()) {
translation->requested = false;
set(translation);
}
} else {
AddComponents(HistoryMessageTranslation::Bit());
const auto added = Get<HistoryMessageTranslation>();
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<HistoryItem*>(this)->cacheOnlyEmojiAndSpaces(
IsOnlyEmojiAndSpaces(_text.text));
}
return (_flags & MessageFlag::OnlyEmojiAndSpaces);
}
void HistoryItem::setupChatThemeChange() {
if (const auto user = history()->peer->asUser()) {
auto link = std::make_shared<LambdaClickHandler>([=](

View File

@ -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 <typename Enum>
@ -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<ClickHandlerPtr> &customTextLinks() const;
[[nodiscard]] TextForMimeData clipboardText() const;
@ -397,6 +402,10 @@ public:
[[nodiscard]] bool requiresSendInlineRight() const;
[[nodiscard]] std::optional<QString> errorTextForForward(
not_null<Data::Thread*> 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<HistoryMessageTranslation*> translation,
bool used);
void setSelfDestruct(HistorySelfDestructType type, int ttlSeconds);
TextWithEntities fromLinkText() const;

View File

@ -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<HistoryMessageTranslation, HistoryItem> {
TextWithEntities text;
LanguageId to;
bool requested = false;
bool failed = false;
bool used = false;
};
struct HistoryMessageReplyMarkup
: public RuntimeComponent<HistoryMessageReplyMarkup, HistoryItem> {
using Button = HistoryMessageMarkupButton;

View File

@ -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*> 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;
}

View File

@ -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*> 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*> _history;
LanguageId _offerFrom;
LanguageId _translatedTo;
};

View File

@ -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<HistoryView::TranslateBar>(
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);

View File

@ -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<HistoryItem*> item);
void updatePinnedViewer();
void setupTranslateBar();
void setupPinnedTracker();
void checkPinnedBarState();
void clearHidingPinnedBar();
@ -638,6 +640,9 @@ private:
object_ptr<Ui::IconButton> _fieldBarCancel;
std::unique_ptr<HistoryView::TranslateBar> _translateBar;
int _translateBarHeight = 0;
std::unique_ptr<HistoryView::PinnedTracker> _pinnedTracker;
std::unique_ptr<Ui::PinnedBar> _pinnedBar;
std::unique_ptr<Ui::PinnedBar> _hidingPinnedBar;

View File

@ -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<Ui::PopupMenu> 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)) {

View File

@ -652,6 +652,18 @@ const Ui::Text::String &Element::text() const {
return _text;
}
OnlyEmojiAndSpaces Element::isOnlyEmojiAndSpaces() const {
if (data()->Has<HistoryMessageTranslation>()) {
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()) {

View File

@ -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);

View File

@ -37,6 +37,7 @@ struct ToPreviewOptions {
bool generateImages = true;
bool ignoreGroup = false;
bool ignoreTopic = true;
bool translated = false;
};
} // namespace HistoryView

View File

@ -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();
}

View File

@ -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 <QtGui/QtEvents>
namespace HistoryView {
TranslateBar::TranslateBar(
not_null<QWidget*> parent,
not_null<History*> history)
: _wrap(parent, object_ptr<Ui::FlatButton>(
parent,
QString(),
st::historyComposeButton))
, _shadow(std::make_unique<Ui::PlainShadow>(_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<QRect(QRect)> 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*> history) {
_wrap.geometryValue(
) | rpl::start_with_next([=](QRect rect) {
updateShadowGeometry(rect);
updateControlsGeometry(rect);
}, _wrap.lifetime());
const auto button = static_cast<Ui::FlatButton*>(_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<Ui::FlatLabel>(
button,
st::historyTranslateLabel);
const auto icon = Ui::CreateChild<Ui::RpWidget>(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<Ui::IconButton>(
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<int> TranslateBar::heightValue() const {
return _wrap.heightValue();
}
} // namespace Ui

View File

@ -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<QWidget*> parent, not_null<History*> history);
~TranslateBar();
void show();
void hide();
void raise();
void finishAnimating();
void setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);
void move(int x, int y);
void resizeToWidth(int width);
[[nodiscard]] int height() const;
[[nodiscard]] rpl::producer<int> heightValue() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _wrap.lifetime();
}
private:
void setup(not_null<History*> history);
void updateShadowGeometry(QRect wrapGeometry);
void updateControlsGeometry(QRect wrapGeometry);
Ui::SlideWrap<> _wrap;
std::unique_ptr<Ui::PlainShadow> _shadow;
Fn<QRect(QRect)> _shadowGeometryPostprocess;
bool _shouldBeShown = false;
bool _forceHidden = false;
};
} // namespace HistoryView

View File

@ -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(history)
, _limit(kEnoughForRecognition) {
setup();
}
TranslateTracker::~TranslateTracker() {
cancelToRequest();
cancelSentRequest();
}
rpl::producer<bool> 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<Element*> 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<HistoryItem*> item,
LanguageId translatedTo) {
add(item, translatedTo, false);
}
void TranslateTracker::add(
not_null<HistoryItem*> 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<HistoryMessageReply>()) {
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<HistoryItem*> 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<MTPint>();
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<MTPint>(list),
MTPVector<MTPTextWithEntities>(),
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<MTPTextWithEntities> &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<QString>(&entry.id)) {
entry.id = Platform::Language::Recognize(*text);
}
}
}
void TranslateTracker::trackSkipLanguages() {
Core::App().settings().skipTranslationLanguagesValue(
) | rpl::start_with_next([=](const std::vector<LanguageId> &skip) {
checkRecognized(skip);
}, _trackingLifetime);
}
void TranslateTracker::checkRecognized() {
checkRecognized(Core::App().settings().skipTranslationLanguages());
}
void TranslateTracker::checkRecognized(const std::vector<LanguageId> &skip) {
if (!_trackingLanguage.current()) {
_history->translateOfferFrom({});
return;
}
auto languages = base::flat_map<LanguageId, int>();
for (const auto &[id, entry] : _itemsForRecognize) {
if (const auto id = std::get_if<LanguageId>(&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<LanguageId, int>::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

View File

@ -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*> history);
~TranslateTracker();
[[nodiscard]] bool enoughForRecognition() const;
void startBunch();
void add(not_null<Element*> view, LanguageId translatedTo);
void add(not_null<HistoryItem*> item, LanguageId translatedTo);
void finishBunch();
[[nodiscard]] rpl::producer<bool> trackingLanguage() const;
private:
using MaybeLanguageId = std::variant<QString, LanguageId>;
struct ItemForRecognize;
struct ItemToRequest {
int length = 0;
};
void setup();
void add(
not_null<HistoryItem*> item,
LanguageId translatedTo,
bool skipDependencies);
void recognizeCollected();
void trackSkipLanguages();
void checkRecognized();
void checkRecognized(const std::vector<LanguageId> &skip);
void applyLimit();
void requestSome();
void cancelToRequest();
void cancelSentRequest();
void switchTranslation(not_null<HistoryItem*> item, LanguageId id);
void requestDone(
LanguageId to,
const QVector<MTPTextWithEntities> &list);
const not_null<History*> _history;
rpl::variable<bool> _trackingLanguage = false;
base::flat_map<FullMsgId, ItemForRecognize> _itemsForRecognize;
uint64 _generation = 0;
int _limit = 0;
int _addedInBunch = -1;
bool _allLoaded = false;
base::flat_map<not_null<HistoryItem*>, LanguageId> _switchTranslations;
base::flat_map<FullMsgId, ItemToRequest> _itemsToRequest;
std::vector<FullMsgId> _requested;
mtpRequestId _requestId = 0;
rpl::lifetime _trackingLifetime;
rpl::lifetime _lifetime;
};
} // namespace HistoryView

View File

@ -305,7 +305,7 @@ Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
};
result.setMarkedText(
st::messageTextStyle,
item->originalTextWithLocalEntities(),
item->translatedTextWithLocalEntities(),
Ui::ItemTextOptions(item),
context);
FillTextWithAnimatedSpoilers(_parent, result);

View File

@ -2300,7 +2300,7 @@ void OverlayWidget::refreshCaption() {
return;
}
}
const auto caption = _message->originalText();
const auto caption = _message->translatedText();
if (caption.text.isEmpty()) {
return;
}

View File

@ -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;
}

@ -1 +1 @@
Subproject commit cf59ca87b761ab5bbd80be02a61cd38a70142898
Subproject commit e5ac664fe397d5874a244bbbc8a7b266223cb88b

@ -1 +1 @@
Subproject commit 43e9128014c5239a6732ae34bdfe007efb9692c8
Subproject commit f2e698f2209a86c133261196275ca98273c7a4dc