/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/translate_box.h" #include "api/api_text_entities.h" // Api::EntitiesToMTP / EntitiesFromMTP. #include "core/application.h" #include "core/core_settings.h" #include "core/ui_integration.h" #include "data/data_peer.h" #include "data/data_session.h" #include "history/history.h" #include "lang/lang_instance.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "mtproto/sender.h" #include "settings/settings_common.h" #include "spellcheck/platform/platform_language.h" #include "ui/boxes/choose_language_box.h" #include "ui/effects/loading_element.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/multi_select.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_info.h" // inviteLinkListItem. #include "styles/style_layers.h" #include "styles/style_settings.h" // settingsSubsectionTitlePadding. #include namespace Ui { namespace { constexpr auto kSkipAtLeastOneDuration = 3 * crl::time(1000); class ShowButton final : public RpWidget { public: ShowButton(not_null parent); [[nodiscard]] rpl::producer clicks() const; protected: void paintEvent(QPaintEvent *e) override; private: LinkButton _button; }; ShowButton::ShowButton(not_null parent) : RpWidget(parent) , _button(this, tr::lng_usernames_activate_confirm(tr::now)) { _button.sizeValue( ) | rpl::start_with_next([=](const QSize &s) { resize( s.width() + st::defaultEmojiSuggestions.fadeRight.width(), s.height()); _button.moveToRight(0, 0); }, lifetime()); _button.show(); } void ShowButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto clip = e->rect(); const auto &icon = st::defaultEmojiSuggestions.fadeRight; const auto fade = QRect(0, 0, icon.width(), height()); if (fade.intersects(clip)) { icon.fill(p, fade); } const auto fill = clip.intersected( { icon.width(), 0, width() - icon.width(), height() }); if (!fill.isEmpty()) { p.fillRect(fill, st::boxBg); } } rpl::producer ShowButton::clicks() const { return _button.clicks(); } } // namespace void TranslateBox( not_null box, not_null peer, MsgId msgId, TextWithEntities text, bool hasCopyRestriction) { box->setWidth(st::boxWideWidth); box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); const auto container = box->verticalLayout(); struct State { State(not_null session) : api(&session->mtp()) { } MTP::Sender api; rpl::variable to; }; const auto state = box->lifetime().make_state(&peer->session()); state->to = ChooseTranslateTo(peer->owner().history(peer)); if (!IsServerMsgId(msgId)) { msgId = 0; } using Flag = MTPmessages_TranslateText::Flag; const auto flags = msgId ? (Flag::f_peer | Flag::f_id) : !text.text.isEmpty() ? Flag::f_text : Flag(0); const auto &stLabel = st::aboutLabel; const auto lineHeight = stLabel.style.lineHeight; Settings::AddSkip(container); // Settings::AddSubsectionTitle( // container, // tr::lng_translate_box_original()); const auto original = box->addRow(object_ptr>( box, object_ptr(box, stLabel))); { if (hasCopyRestriction) { original->entity()->setContextMenuHook([](auto&&) { }); } original->entity()->setMarkedText( text, Core::MarkedTextContext{ .session = &peer->session(), .customEmojiRepaint = [=] { original->entity()->update(); }, }); original->setMinimalHeight(lineHeight); original->hide(anim::type::instant); const auto show = Ui::CreateChild>( container.get(), object_ptr(container)); show->hide(anim::type::instant); rpl::combine( container->widthValue(), original->geometryValue() ) | rpl::start_with_next([=](int width, const QRect &rect) { show->moveToLeft( width - show->width() - st::boxRowPadding.right(), rect.y() + std::abs(lineHeight - show->height()) / 2); }, show->lifetime()); original->entity()->heightValue( ) | rpl::filter([](int height) { return height > 0; }) | rpl::take(1) | rpl::start_with_next([=](int height) { if (height > lineHeight) { show->show(anim::type::instant); } }, show->lifetime()); show->toggleOn(show->entity()->clicks() | rpl::map_to(false)); original->toggleOn(show->entity()->clicks() | rpl::map_to(true)); } Settings::AddSkip(container); Settings::AddSkip(container); Settings::AddDivider(container); Settings::AddSkip(container); { const auto padding = st::settingsSubsectionTitlePadding; const auto subtitle = Settings::AddSubsectionTitle( container, state->to.value() | rpl::map(LanguageName)); // Workaround. state->to.value() | rpl::start_with_next([=] { subtitle->resizeToWidth(container->width() - padding.left() - padding.right()); }, subtitle->lifetime()); } const auto translated = box->addRow(object_ptr>( box, object_ptr(box, stLabel))); translated->entity()->setSelectable(!hasCopyRestriction); constexpr auto kMaxLines = 3; container->resizeToWidth(box->width()); const auto loading = box->addRow(object_ptr>( box, CreateLoadingTextWidget( box, st::aboutLabel, std::min(original->entity()->height() / lineHeight, kMaxLines), state->to.value() | rpl::map([=](LanguageId id) { return id.locale().textDirection() == Qt::RightToLeft; })))); const auto showText = [=](TextWithEntities text) { const auto label = translated->entity(); label->setMarkedText( text, Core::MarkedTextContext{ .session = &peer->session(), .customEmojiRepaint = [=] { label->update(); }, }); translated->show(anim::type::instant); loading->hide(anim::type::instant); }; const auto send = [=](LanguageId to) { loading->show(anim::type::instant); translated->hide(anim::type::instant); state->api.request(MTPmessages_TranslateText( MTP_flags(flags), msgId ? peer->input : MTP_inputPeerEmpty(), (msgId ? MTP_vector(1, MTP_int(msgId)) : MTPVector()), (msgId ? MTPVector() : MTP_vector(1, MTP_textWithEntities( MTP_string(text.text), Api::EntitiesToMTP( &peer->session(), text.entities, Api::ConvertOption::SkipLocal)))), MTP_string(to.twoLetterCode()) )).done([=](const MTPmessages_TranslatedText &result) { const auto &data = result.data(); const auto &list = data.vresult().v; if (list.isEmpty()) { showText( Ui::Text::Italic(tr::lng_translate_box_error(tr::now))); } else { showText(TextWithEntities{ .text = qs(list.front().data().vtext()), .entities = Api::EntitiesFromMTP( &peer->session(), list.front().data().ventities().v), }); } }).fail([=](const MTP::Error &error) { showText( Ui::Text::Italic(tr::lng_translate_box_error(tr::now))); }).send(); }; state->to.value() | rpl::start_with_next(send, box->lifetime()); box->addLeftButton(tr::lng_settings_language(), [=] { if (loading->toggled()) { return; } box->uiShow()->showBox(ChooseTranslateToBox( state->to.current(), crl::guard(box, [=](LanguageId id) { state->to = id; }))); }); } bool SkipTranslate(TextWithEntities textWithEntities) { const auto &text = textWithEntities.text; if (text.isEmpty()) { return true; } if (!Core::App().settings().translateButtonEnabled()) { return true; } constexpr auto kFirstChunk = size_t(100); auto hasLetters = (text.size() >= kFirstChunk); for (auto i = 0; i < kFirstChunk; i++) { if (i >= text.size()) { break; } if (text.at(i).isLetter()) { hasLetters = true; break; } } if (!hasLetters) { return true; } #ifndef TDESKTOP_DISABLE_SPELLCHECK const auto result = Platform::Language::Recognize(text); const auto skip = Core::App().settings().skipTranslationLanguages(); return result.known() && ranges::contains(skip, result); #else return false; #endif } object_ptr EditSkipTranslationLanguages() { auto title = tr::lng_translate_settings_choose(); const auto selected = std::make_shared>( Core::App().settings().skipTranslationLanguages()); const auto weak = std::make_shared>(); const auto check = [=](LanguageId id) { const auto already = ranges::contains(*selected, id); if (already) { selected->erase(ranges::remove(*selected, id), selected->end()); } else { selected->push_back(id); } if (already && selected->empty()) { if (const auto strong = weak->data()) { strong->showToast( tr::lng_translate_settings_one(tr::now), kSkipAtLeastOneDuration); } return false; } return true; }; auto result = Box(ChooseLanguageBox, std::move(title), [=]( std::vector &&list) { Core::App().settings().setSkipTranslationLanguages( std::move(list)); Core::App().saveSettingsDelayed(); }, *selected, true, check); *weak = result.data(); return result; } object_ptr ChooseTranslateToBox( LanguageId bringUp, Fn callback) { auto &settings = Core::App().settings(); auto selected = std::vector{ settings.translateTo(), }; for (const auto &id : settings.skipTranslationLanguages()) { if (id != selected.front()) { selected.push_back(id); } } if (bringUp && ranges::contains(selected, bringUp)) { selected.push_back(bringUp); } return Box(ChooseLanguageBox, tr::lng_languages(), [=]( const std::vector &ids) { Expects(!ids.empty()); const auto id = ids.front(); Core::App().settings().setTranslateTo(id); Core::App().saveSettingsDelayed(); callback(id); }, selected, false, nullptr); } LanguageId ChooseTranslateTo(not_null history) { return ChooseTranslateTo(history->translateOfferedFrom()); } LanguageId ChooseTranslateTo(LanguageId offeredFrom) { auto &settings = Core::App().settings(); return ChooseTranslateTo( offeredFrom, settings.translateTo(), settings.skipTranslationLanguages()); } LanguageId ChooseTranslateTo( not_null history, LanguageId savedTo, const std::vector &skip) { return ChooseTranslateTo(history->translateOfferedFrom(), savedTo, skip); } LanguageId ChooseTranslateTo( LanguageId offeredFrom, LanguageId savedTo, const std::vector &skip) { return (offeredFrom != savedTo) ? savedTo : skip.front(); } } // namespace Ui