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