diff --git a/Telegram/Resources/animations/cocoon.tgs b/Telegram/Resources/animations/cocoon.tgs new file mode 100644 index 0000000000..9eba8213ad Binary files /dev/null and b/Telegram/Resources/animations/cocoon.tgs differ diff --git a/Telegram/Resources/art/cocoon.webp b/Telegram/Resources/art/cocoon.webp new file mode 100644 index 0000000000..060239730b Binary files /dev/null and b/Telegram/Resources/art/cocoon.webp differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b893ac30c8..dc596196c3 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4971,6 +4971,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_translate_settings" = "Settings"; "lng_translate_undo" = "Undo"; +"lng_translate_cocoon_menu" = "Translations are powered by\n**{emoji} Cocoon**. {link}"; +"lng_translate_cocoon_link" = "How does it work?"; +"lng_translate_cocoon_title" = "Cocoon"; +"lng_translate_cocoon_subtitle" = "**Cocoon** ({text}) handles AI tasks **safely** and **efficiently**."; +"lng_translate_cocoon_explain" = "**Co**nfidential **Co**mpute **O**pen **N**etwork"; +"lng_translate_cocoon_private_title" = "Private"; +"lng_translate_cocoon_private_text" = "No third party can access any data, such as translations, inside {mention}."; +"lng_translate_cocoon_private_mention" = "@cocoon"; +"lng_translate_cocoon_efficient_title" = "Efficient"; +"lng_translate_cocoon_efficient_text" = "Cocoon has allowed Telegram to reduce translation costs by 6x."; +"lng_translate_cocoon_everyone_title" = "For Everyone"; +"lng_translate_cocoon_everyone_text" = "Any developer can use Cocoon for AI features. Learn more at {domain}."; +"lng_translate_cocoon_everyone_domain" = "cocoon.org"; +"lng_translate_cocoon_text" = "Want to integrate Cocoon into your projects?\nReach out at {link}"; +"lng_translate_cocoon_text_link" = "t.me/cocoon?direct"; +"lng_translate_cocoon_done" = "Understood"; + "lng_downloads_section" = "Downloads"; "lng_downloads_view_in_chat" = "View in chat"; "lng_downloads_view_in_section" = "View in downloads"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index f448e9e4a5..a33a9ff016 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -52,6 +52,7 @@ ../../animations/show_or_premium_readtime.tgs ../../animations/passkeys.tgs ../../animations/ban.tgs + ../../animations/cocoon.tgs ../../animations/profile/profile_muting.tgs ../../animations/profile/profile_unmuting.tgs diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index ca37cc179a..fd3f69e5ed 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -9,6 +9,7 @@ ../../art/logo_256_no_margin.png ../../art/themeimage.jpg ../../art/round_placeholder.jpg + ../../art/cocoon.webp ../../day-blue.tdesktop-theme ../../night.tdesktop-theme ../../night-green.tdesktop-theme diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index 04ceda97cb..0c62f7680a 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -354,10 +354,37 @@ not_null GenerateLocalSticker( not_null GenerateLocalTgsSticker( not_null session, const QString &name) { + const auto cache = [&] { + struct Session { + base::weak_ptr session; + base::flat_map> cache; + }; + static auto Map = std::vector(); + for (auto i = begin(Map); i != end(Map);) { + if (const auto strong = i->session.get()) { + if (strong == session) { + return &i->cache; + } + ++i; + } else { + i = Map.erase(i); + } + } + Map.push_back({ .session = session }); + return &Map.back().cache; + }(); + + const auto i = cache->find(name); + if (i != end(*cache)) { + return i->second; + } + const auto result = GenerateLocalSticker( session, u":/animations/"_q + name + u".tgs"_q); + cache->emplace(name, result); + Ensures(result->sticker()->isLottie()); return result; } diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 48085dc18b..83d4ab0f08 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -7,19 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_translate_bar.h" +#include "boxes/star_gift_box.h" // AddUniqueCloseButton #include "boxes/translate_box.h" +#include "chat_helpers/stickers_lottie.h" #include "core/application.h" #include "core/core_settings.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_changes.h" +#include "data/data_document.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_credits_graphics.h" // CreditsEntryBoxStyleOverrides +#include "ui/controls/feature_list.h" +#include "ui/effects/ministar_particles.h" #include "ui/effects/ripple_animation.h" #include "ui/boxes/choose_language_box.h" // EditSkipTranslationLanguages. #include "ui/layers/box_content.h" +#include "ui/layers/generic_box.h" #include "ui/widgets/menu/menu_item_base.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" +#include "ui/top_background_gradient.h" +#include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" @@ -27,6 +38,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_info.h" // infoStarsUnderstood +#include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include @@ -80,6 +93,247 @@ TextParseOptions MenuTextOptions = { Qt::LayoutDirectionAuto, // dir }; +void AddCocoonBoxCover(not_null container) { + const auto cover = container->add(object_ptr(container)); + + static const auto gradientEdge = QColor(0x06, 0x11, 0x29); + static const auto gradientCenter = QColor(0x09, 0x16, 0x43); + + static const auto gradientLeft = QColor(0x68, 0xc9, 0xff); + static const auto gradientRight = QColor(0xcb, 0x57, 0xff); + + static const auto textColor = QColor(0xb8, 0xc9, 0xef); + static const auto boldColor = QColor(0xff, 0xff, 0xff); + + const auto logoTop = st::cocoonLogoTop; + const auto logoSize = st::cocoonLogoSize; + const auto ratio = style::DevicePixelRatio(); + auto logo = QImage(u":/gui/art/cocoon.webp"_q).scaled( + QSize(logoSize, logoSize) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + logo.setDevicePixelRatio(ratio); + const auto titleTop = logoTop + logoSize + st::cocoonTitleTop; + const auto subtitleTop = titleTop + + st::cocoonTitleFont->height + + st::cocoonSubtitleTop; + + const auto boldToColorized = [](QString text) { + auto result = tr::rich(text); + auto &entities = result.entities; + for (auto i = entities.begin(); i != entities.end(); ++i) { + if (i->type() == EntityType::Bold) { + i = entities.insert( + i, + EntityInText( + EntityType::Colorized, + i->offset(), + i->length())); + ++i; + } + } + return result; + }; + + struct State { + QImage gradient; + Ui::Animations::Basic animation; + std::optional particles; + style::owned_color subtitleFg = style::owned_color{ textColor }; + style::owned_color subtitleBoldFg = style::owned_color{ boldColor }; + style::FlatLabel subtitleSt = st::cocoonSubtitle; + }; + const auto state = cover->lifetime().make_state(); + state->subtitleSt.textFg = state->subtitleFg.color(); + state->subtitleSt.palette.linkFg = state->subtitleBoldFg.color(); + state->animation.init([=] { + cover->update(); + if (anim::Disabled()) { + state->animation.stop(); + } + }); + + constexpr auto kParticlesCount = 50; + state->particles.emplace( + Ui::StarParticles::Type::RadialInside, + kParticlesCount, + st::cocoonLogoSize / 12); + state->particles->setSpeed(0.05); + auto particleColors = std::vector(); + constexpr auto kParticleColors = 12; + particleColors.reserve(kParticleColors); + for (auto i = 0; i != kParticleColors; ++i) { + particleColors.push_back(anim::color( + gradientLeft, + gradientRight, + float64(i) / (kParticleColors - 1))); + } + state->particles->setColors(std::move(particleColors)); + + const auto subtitle = CreateChild( + cover, + tr::lng_translate_cocoon_subtitle( + lt_text, + tr::lng_translate_cocoon_explain(boldToColorized), + tr::rich), + state->subtitleSt); + subtitle->setTryMakeSimilarLines(true); + + cover->widthValue() | rpl::on_next([=](int width) { + const auto available = width + - st::boxRowPadding.left() + - st::boxRowPadding.right(); + subtitle->resizeToWidth(available); + subtitle->moveToLeft(st::boxRowPadding.left(), subtitleTop); + + const auto bottom = st::cocoonSubtitleBottom; + cover->resize(width, subtitle->y() + subtitle->height() + bottom); + }, cover->lifetime()); + + cover->paintRequest() | rpl::on_next([=] { + auto p = Painter(cover); + + const auto width = cover->width(); + const auto ratio = style::DevicePixelRatio(); + if (state->gradient.size() != cover->size() * ratio) { + state->gradient = Ui::CreateTopBgGradient( + cover->size(), + gradientCenter, + gradientEdge); + + auto font = st::cocoonTitleFont->f; + font.setWeight(QFont::Bold); + auto metrics = QFontMetrics(font); + + const auto text = tr::lng_translate_cocoon_title(tr::now); + const auto textw = metrics.width(text); + const auto left = (width - textw) / 2; + + auto q = QPainter(&state->gradient); + auto hq = PainterHighQualityEnabler(q); + + auto gradient = QLinearGradient(left, 0, left + textw, 0); + gradient.setStops({ + { 0., gradientLeft }, + { 1., gradientRight }, + }); + auto pen = QPen(QBrush(gradient), 1.); + q.setPen(pen); + q.setFont(font); + q.setBrush(Qt::NoBrush); + + q.drawText(left, titleTop + metrics.ascent(), text); + } + p.drawImage(0, 0, state->gradient); + + const auto logoRect = QRect( + (width - logoSize) / 2, + logoTop, + logoSize, + logoSize); + const auto paddingAdd = int(base::SafeRound(logoTop * 1.2)); + const auto particlesRect = logoRect.marginsAdded( + { paddingAdd, paddingAdd, paddingAdd, paddingAdd }); + + state->particles->paint(p, particlesRect, crl::now()); + if (!anim::Disabled() && !state->animation.animating()) { + state->animation.start(); + } + p.drawImage(logoRect, logo); + }, cover->lifetime()); +} + +struct CocoonLinkInfo { + QString text; + QString link; +}; + +[[nodiscard]] CocoonLinkInfo CocoonMention() { + const auto mention = tr::lng_translate_cocoon_private_mention(tr::now); + const auto username = QString(mention).replace('@', QString()); + const auto link = u"tg://resolve?domain="_q + username; + return { mention, link }; +} + +[[nodiscard]] CocoonLinkInfo CocoonDomain() { + const auto domain = tr::lng_translate_cocoon_everyone_domain(tr::now); + const auto link = u"https://"_q + domain; + return { domain, link }; +} + +[[nodiscard]] CocoonLinkInfo CocoonDirect() { + const auto text = tr::lng_translate_cocoon_text_link(tr::now); + const auto link = u"https://"_q + text; + return { text, link }; +} + +void AboutCocoonBox(not_null box) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::stakeBox); + box->setNoContentMargin(true); + + const auto container = box->verticalLayout(); + AddCocoonBoxCover(container); + + AddUniqueCloseButton(box, {}); + + const auto mention = CocoonMention(); + const auto domain = CocoonDomain(); + const auto direct = CocoonDirect(); + const auto features = std::vector{ + { + st::menuIconLock, + tr::lng_translate_cocoon_private_title(tr::now), + tr::lng_translate_cocoon_private_text( + tr::now, + lt_mention, + tr::link(mention.text, mention.link), + tr::rich), + }, + { + st::menuIconStats, + tr::lng_translate_cocoon_efficient_title(tr::now), + tr::lng_translate_cocoon_efficient_text(tr::now, tr::rich), + }, + { + st::menuIconGiftPremium, + tr::lng_translate_cocoon_everyone_title(tr::now), + tr::lng_translate_cocoon_everyone_text( + tr::now, + lt_domain, + tr::link(domain.text, domain.link), + tr::rich), + }, + }; + auto margin = QMargins(0, st::defaultVerticalListSkip, 0, 0); + for (const auto &feature : features) { + box->addRow( + MakeFeatureListEntry(box, feature), + st::boxRowPadding + margin); + margin = {}; + } + + box->addRow( + object_ptr(box), + st::cocoonJoinSeparatorPadding); + + box->addRow( + object_ptr( + box, + tr::lng_translate_cocoon_text( + lt_link, + rpl::single(tr::link(direct.text, direct.link)), + tr::rich), + st::cocoonJoinLabel), + style::al_top); + + box->addButton(rpl::single(QString()), [=] { + box->closeBox(); + })->setText(rpl::single(Ui::Text::IconEmoji( + &st::infoStarsUnderstood + ).append(' ').append(tr::lng_translate_cocoon_done(tr::now)))); +} + TwoTextAction::TwoTextAction( not_null parent, const style::Menu &st, @@ -476,6 +730,36 @@ void TranslateBar::showMenu(base::unique_qptr menu) { tr::lng_translate_menu_hide(tr::now), hideBar, &st::menuIconCancel); + _menu->addSeparator(); + + const auto cocoon = ChatHelpers::GenerateLocalTgsSticker( + &_history->session(), + u"cocoon"_q); + cocoon->overrideEmojiUsesTextColor(true); + auto item = base::make_unique_q( + _menu, + st::defaultMenu, + st::historyTranslateCocoonLabel, + QPoint( + st::defaultMenu.itemPadding.left(), + st::defaultMenu.itemPadding.top()), + tr::lng_translate_cocoon_menu( + tr::now, + lt_emoji, + Data::SingleCustomEmoji(cocoon), + lt_link, + tr::link(tr::lng_translate_cocoon_link(tr::now, tr::bold)), + tr::rich), + Core::TextContext({ + .session = &_history->session(), + .customEmojiLoopLimit = -1, + })); + item->clicks( + ) | rpl::on_next([controller = _controller] { + controller->show(Box(AboutCocoonBox)); + }, item->lifetime()); + _menu->addAction(std::move(item)); + _menu->popup(_wrap.mapToGlobal( QPoint(_wrap.width(), 0) + st::historyTranslateMenuPosition)); } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 5c8d9de706..8b13683746 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -971,6 +971,32 @@ historyTranslateSettings: IconButton(defaultIconButton) { } } historyTranslateMenuPosition: point(-6px, 30px); +historyTranslateCocoonLabel: FlatLabel(historyHasCustomEmoji) { + style: TextStyle(defaultTextStyle) { + font: font(12px); + } + margin: margins(4px, 8px, 4px, 8px); +} + +cocoonLogoTop: 32px; +cocoonLogoSize: 96px; +cocoonTitleFont: font(36px semibold); +cocoonTitleTop: 12px; +cocoonSubtitle: FlatLabel(defaultFlatLabel) { + minWidth: 120px; + align: align(top); +} +cocoonSubtitleTop: 8px; +cocoonSubtitleBottom: 16px; +cocoonJoinSeparatorPadding: margins(24px, 4px, 24px, 12px); +cocoonJoinLabel: FlatLabel(defaultFlatLabel) { + style: TextStyle(defaultTextStyle) { + font: font(12px); + } + textFg: windowSubTextFg; + minWidth: 120px; + align: align(top); +} historySponsoredAboutMenuLabelPosition: point(54px, 4px); diff --git a/Telegram/SourceFiles/ui/controls/feature_list.cpp b/Telegram/SourceFiles/ui/controls/feature_list.cpp index 5ab504bab9..dcb31ce8df 100644 --- a/Telegram/SourceFiles/ui/controls/feature_list.cpp +++ b/Telegram/SourceFiles/ui/controls/feature_list.cpp @@ -42,6 +42,7 @@ object_ptr MakeFeatureListEntry( icon->show(); title->show(); about->show(); + about->setLinksTrusted(); widget->widthValue( ) | rpl::on_next([=](int width) { const auto left = st::infoStarsFeatureLabelLeft; diff --git a/Telegram/SourceFiles/ui/effects/ministar_particles.cpp b/Telegram/SourceFiles/ui/effects/ministar_particles.cpp index 1922044c0f..13214ad55c 100644 --- a/Telegram/SourceFiles/ui/effects/ministar_particles.cpp +++ b/Telegram/SourceFiles/ui/effects/ministar_particles.cpp @@ -32,7 +32,31 @@ void StarParticles::setVisible(float visible) { } void StarParticles::setColor(const QColor &color) { - _color = color; + setColors({ color }); +} + +void StarParticles::setColors(std::vector colors) { + Expects(!colors.empty()); + + const auto was = int(_colors.size()); + _colors = std::move(colors); + const auto now = int(_colors.size()); + + _paintedCaches.clear(); + + if (now != was) { + if (now > 1) { + auto buffered = base::BufferedRandom(_particles.size()); + for (auto &p : _particles) { + p.colorIndex = base::RandomIndex(now, buffered); + } + } else { + for (auto &p : _particles) { + p.colorIndex = 0; + } + } + + } } QImage StarParticles::generateStarCache(int size, QColor color) { @@ -76,12 +100,14 @@ void StarParticles::generate() { _particles.clear(); _particles.reserve(_count); - constexpr auto kRandomPerParticle = 12; + constexpr auto kRandomPerParticle = 13; auto random = bytes::vector(_count * kRandomPerParticle); base::RandomFill(random.data(), random.size()); auto idx = 0; const auto next = [&] { return uchar(random[idx++]) / 255.; }; + const auto colors = int(_colors.size()); + for (auto i = 0; i < _count; ++i) { auto p = Particle(); p.x = next(); @@ -106,6 +132,10 @@ void StarParticles::generate() { p.vy = -std::sin(angle) * speed; } + if (colors > 1) { + p.colorIndex = int(base::SafeRound(next() * (colors - 1))); + } + _particles.push_back(p); } } @@ -116,13 +146,26 @@ void StarParticles::paint(QPainter &p, const QRect &rect, crl::time now) { return; } - const auto colorKey = _color.rgba(); - const auto i = _starCache.find(colorKey); - const auto &cache = (i != end(_starCache)) - ? i->second - : _starCache.emplace( - colorKey, - generateStarCache(_starSize, _color)).first->second; + const auto validate = [&](int colorIndex) { + Expects(colorIndex >= 0 && colorIndex < _colors.size()); + + if (_paintedCaches.empty()) { + _paintedCaches.resize(_colors.size()); + } else if (const auto result = _paintedCaches[colorIndex] + ; !result.isNull()) { + return result; + } + const auto &color = _colors[colorIndex]; + const auto colorKey = color.rgba(); + const auto i = _starCache.find(colorKey); + const auto &cache = (i != end(_starCache)) + ? i->second + : _starCache.emplace( + colorKey, + generateStarCache(_starSize, color)).first->second; + _paintedCaches[colorIndex] = cache; + return cache; + }; const auto dt = (now - _lastTime) / 1000.; _lastTime = now; @@ -147,6 +190,7 @@ void StarParticles::paint(QPainter &p, const QRect &rect, crl::time now) { const auto x = rect.x() + particle.x * rect.width(); const auto y = rect.y() + particle.y * rect.height(); + const auto cache = validate(particle.colorIndex); const auto size = cache.width() / cache.devicePixelRatio() * particle.size; diff --git a/Telegram/SourceFiles/ui/effects/ministar_particles.h b/Telegram/SourceFiles/ui/effects/ministar_particles.h index 5b667d3534..9fc07d1f2a 100644 --- a/Telegram/SourceFiles/ui/effects/ministar_particles.h +++ b/Telegram/SourceFiles/ui/effects/ministar_particles.h @@ -24,10 +24,12 @@ public: void setSpeed(float speed); void setVisible(float visible); void setColor(const QColor &color); + void setColors(std::vector colors); void paint(QPainter &p, const QRect &rect, crl::time now); private: struct Particle { + int colorIndex = 0; float x = 0.; float y = 0.; float vx = 0.; @@ -47,7 +49,8 @@ private: int _starSize = 0; float _speed = 1.f; float _visible = 1.f; - QColor _color = QColor(255, 200, 70); + std::vector _colors = { QColor(255, 200, 70) }; + std::vector _paintedCaches; std::vector _particles; base::flat_map _starCache; crl::time _lastTime = 0; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 07bc368f17..b5b21bbcc5 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 07bc368f1795e0fa99aed6e18a5256d18badce0a +Subproject commit b5b21bbcc5a7c702b74f9450615fe350824f5fa7